宣布 pytest-test-categories v1.1.0:将 Google 测试哲学引入 Python

发布: (2025年12月5日 GMT+8 07:38)
5 min read
原文: Dev.to

Source: Dev.to

大多数测试套件的问题

说实话——以下哪些情况出现在你的代码库中?

  • ⏱️ CI 流水线慢,因为测试没有时间预算
  • 🎲 由于网络超时、竞争条件或共享状态导致的 flaky 测试
  • 🔺 测试金字塔倒置,慢速集成测试过多
  • 🚫 单元、集成和系统测试之间没有强制的边界

如果你对其中任何一点点头,那你并不孤单。这些是 Python 项目中最常见的测试反模式。

介绍 pytest-test-categories v1.1.0

一个 pytest 插件,将 Google 在《Software Engineering at Google》中经过实战检验的测试哲学带到 Python。

pip install pytest-test-categories

1. 按规模对测试进行分类,并设定明确的资源约束

import pytest
import requests

@pytest.mark.small
def test_pure_function():
    """Must complete in 0"""

@pytest.mark.large
def test_external_api():
    """Full network access, up to 15 minutes"""
    response = requests.get("https://api.example.com")
    assert response.ok

@pytest.mark.xlarge
def test_extended_e2e():
    """Full access, up to 15 minutes, for extensive E2E tests"""
    # Long‑running end‑to‑end workflow
    pass

2. 强制 hermetic(隔离)性

======================================================================
[TC001] Network Violation
======================================================================
Category: SMALL

What happened:
  SMALL test attempted network connection to api.example.com:443

To fix this (choose one):
  • Mock the network call using responses, httpretty, or respx
  • Use dependency injection to provide a fake HTTP client
  • Change test category to @pytest.mark.medium
======================================================================

3. 验证你的测试金字塔

======================== Test Size Distribution ========================
Small:   120 tests (80.0%) - Target: 80% ✓
Medium:   22 tests (14.7%) - Target: 15% ✓
Large:     8 tests ( 5.3%) - Target:  5% ✓
========================================================================

没有 “allow network” 逃生口

插件特意没有 @pytest.mark.allow_network 标记。添加此类标记会违背初衷:

# This defeats the entire purpose
@pytest.mark.small
@pytest.mark.allow_network  # ❌ This marker doesn't exist!
def test_api():
    requests.get("https://api.example.com")  # Still flaky!

相反,使用合适的分类:

@pytest.mark.medium  # ✓ Honest about what the test does
def test_api():
    requests.get("https://api.example.com")

按测试规模划分的资源矩阵

资源SmallMediumLargeXLarge
时间1 s5 min15 min15 min
网络❌ 阻止本地主机 ✓✓ 允许✓ 允许
文件系统❌ 阻止✓ 允许✓ 允许✓ 允许
数据库❌ 阻止✓ 允许✓ 允许✓ 允许
子进程❌ 阻止✓ 允许✓ 允许✓ 允许
Sleep❌ 阻止✓ 允许✓ 允许✓ 允许

小测试中的 Mock

小测试可以使用 mocking 库(如 responsesrespxpytest-mockpyfakefsVCR.py),不会触发违规,因为 mock 在库层拦截,阻止了对真实资源的访问:

import pytest
import responses
import requests

@pytest.mark.small
@responses.activate
def test_api_with_mock():
    """This is hermetic – no real network call is made."""
    responses.add(
        responses.GET,
        "https://api.example.com/users",
        json={"users": []},
        status=200,
    )

    response = requests.get("https://api.example.com/users")
    assert response.json() == {"users": []}

对于小测试中的文件系统操作,使用 pyfakefs 或内存对象(如 io.StringIO / io.BytesIO)。

渐进式强制执行 rollout

不必在第一天就全部严格:

# pyproject.toml

# 第 1 周:发现 – 查看哪些会失败
[tool.pytest.ini_options]
test_categories_enforcement = "off"

# 第 2‑4 周:迁移 – 逐步修复违规
test_categories_enforcement = "warn"

# 第 5 周起:强制 – 违规导致构建失败
test_categories_enforcement = "strict"

并行执行支持

使用 pytest-xdist 完全支持并行运行已分类的测试:

pytest -n auto

分布统计会在各工作进程间正确聚合,计时隔离防止竞争条件。

导出机器可读报告

生成 JSON 报告以供仪表盘和 CI 集成使用:

pytest --test-size-report=json --test-size-report-file=report.json
{
  "summary": {
    "total_tests": 150,
    "distribution": {
      "small": { "count": 120, "percentage": 80.0 },
      "medium": { "count": 22, "percentage": 14.67 },
      "large": { "count": 8, "percentage": 5.33 }
    },
    "violations": {
      "timing": 0,
      "hermeticity": {
        "network": 0,
        "filesystem": 0,
        "subprocess": 0,
        "database": 0,
        "sleep": 0,
        "total": 0
      }
    }
  }
}

安装与快速入门

pip install pytest-test-categories

pyproject.toml 中启用强制执行:

[tool.pytest.ini_options]
test_categories_enforcement = "warn"

资源

Back to Blog

相关文章

阅读更多 »