何时使用
- 为 Python 函数/类写单元测试,或搭建测试套件与测试基础设施。
- 实践 TDD(先写测试再实现),或为 API/服务写集成测试。
- 需要 mock 外部依赖(HTTP、环境变量、对象属性),测试异步代码、数据库操作。
- 引入参数化降低重复,或用属性测试(hypothesis)覆盖更大输入空间。
- 配置 CI/CD 持续测试与覆盖率门槛,排查失败用例。
不该用:
- 任务与 Python 测试无关,或目标语言是 JS/TS(改用
javascript-testing-patterns)等其他栈。 - 把通过测试当成环境特定验证、上线验收或专家评审的替代品——测试只覆盖你断言到的路径。
- 缺少明确的被测目标、成功判据或所需输入时,先澄清再动手。
步骤 / 指令
- 选测试类型:单元(隔离测函数/类)/ 集成(测组件交互)/ 端到端 / 性能。优先单元,金字塔下宽上窄。
- 单测按 AAA 写:Arrange 准备数据与前置 → Act 执行被测代码 → Assert 验证结果。命名描述「行为」:
test_login_fails_with_invalid_password,别用test_1。 - 重复的 setup/teardown 抽成
@pytest.fixture(用yield分隔 setup/teardown),按需设scope(function/module/session);跨文件共享放conftest.py。 - 同逻辑多输入用
@pytest.mark.parametrize,特殊用例用pytest.param(..., id=...)命名。 - 隔离外部副作用:HTTP/SDK 用
unittest.mock(patch/Mock/side_effect);环境变量与对象属性用内置monkeypatch(setenv/delenv/setattr);临时文件用tmp_path。 - 异常路径用
pytest.raises(Exc, match="..."),需要细节时as exc_info取exc_info.value。 - 异步代码用
pytest-asyncio:测试与 fixture 加@pytest.mark.asyncio,并发用asyncio.gather。 - 用
marks(slow/integration/...)分类,pytest.ini里--strict-markers防拼错;跑pytest -m "not slow"等筛选。 - 接
pytest-cov量覆盖率,CI 上--cov-fail-under=80卡门槛,关注质量而非纯百分比。 - 保持测试独立:无共享状态、各自清理;测试先行或与代码同步写。
示例
最小用例 + fixture + 参数化 + mock + 异常:
# test_demo.py
import pytest
from unittest.mock import patch, Mock
import requests
class Calculator:
def add(self, a, b): return a + b
def divide(self, a, b):
if b == 0: raise ValueError("Cannot divide by zero")
return a / b
@pytest.fixture
def calc(): # setup → yield → teardown
c = Calculator()
yield c
@pytest.mark.parametrize("a,b,expected", [(2, 3, 5), (-1, 1, 0), (0, 0, 0)])
def test_add(calc, a, b, expected): # AAA + 参数化
assert calc.add(a, b) == expected
def test_divide_by_zero(calc): # 测异常
with pytest.raises(ValueError, match="Cannot divide by zero"):
calc.divide(5, 0)
def test_http_mock(): # mock 外部依赖
resp = Mock()
resp.json.return_value = {"id": 1}
resp.raise_for_status.return_value = None
with patch("requests.get", return_value=resp) as m:
assert requests.get("http://x/users/1").json()["id"] == 1
m.assert_called_once_with("http://x/users/1")
环境变量 / 临时文件 / 异步:
def test_env(monkeypatch):
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
monkeypatch.delenv("CACHE", raising=False)
def test_file(tmp_path):
f = tmp_path / "a.txt"; f.write_text("hi")
assert f.read_text() == "hi"
@pytest.mark.asyncio
async def test_async():
import asyncio
results = await asyncio.gather(*(asyncio.sleep(0, r) for r in (1, 2, 3)))
assert results == [1, 2, 3]
属性测试(hypothesis):
from hypothesis import given, strategies as st
@given(st.text())
def test_reverse_twice(s):
assert s[::-1][::-1] == s
覆盖率与配置:
pip install pytest-cov
pytest --cov=myapp --cov-report=term-missing --cov-fail-under=80 tests/
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
addopts = -v --strict-markers --tb=short --cov=myapp --cov-report=term-missing
markers =
slow: marks tests as slow
integration: marks integration tests
注意事项
- 隔离优先:测试间不共享可变状态,每个用例自清理;fixture 用
yield而非 setup/teardown 函数,确保异常时也能清理。 - mock 打在使用处:
patch的目标是「被测模块引用该符号的路径」,不是定义处(例如被测代码import requests后用requests.get,则 patchrequests.get)。 - 断言尽量一个用例一个行为;多断言时确保它们描述同一行为,否则拆开。
--strict-markers防止 marker 拼写错误被静默忽略;新 marker 要在配置里登记。- 覆盖率是手段不是目的:80% 绿了不代表逻辑对,重点补「未测的分支与错误路径」。
- 异步测试需装
pytest-asyncio且函数/fixture 都打@pytest.mark.asyncio,否则协程不会被 await。 - 测试不能替代环境特定验证;缺前置条件、权限或成功判据时停下来澄清。
互见
- related:
async-python-patterns—— 异步代码的测试常配合此栈的 asyncio 写法。 - related:
javascript-testing-patterns—— 同源姊妹篇,JS/TS 侧的等价测试模式。 - combines_with:
test-coverage-gap-finder—— 找出未覆盖路径后回到本技能补齐用例。 - combines_with:
ci-cd-pipeline-builder—— 把 pytest + 覆盖率门槛接入 CI/CD 持续运行。 - combines_with:
systematic-debugger—— 用例失败时用系统化排错定位根因。
采编自 sickn33/antigravity-awesome-skills(MIT)。