Python 测试分析参考手册:pytest 与 unittest 的检测模式与断言校准指南(test-analysis-extensions / python.md)
【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills
本文基于 dotnet-test 插件中 test-analysis-extensions/extensions/python.md 整理而成。该文件是供断言质量、反模式检测、测试缺口分析、测试异味检测与测试打标等多语言测试分析技能消费的“Python 参考数据”,统一描述了 pytest 与 unittest 两大框架下的测试发现、断言 API、跳过注解、等待模式、Mystery Guest 耦合、集成标记、Setup/Teardown 与标签能力。读完本文,你将掌握一套可直接套用的 Python 测试静态分析清单,以及这些规则在当前仓库的哪些技能与 Agent 流水线中被实际调用。
一、这份参考数据在仓库中的角色
python.md不是一份独立的分析技能,而是test-analysis-extensions技能的扩展数据文件。该技能本身标记为user-invocable: false与disable-model-invocation: true,其 SKILL.md 明确指出:它只负责向其他技能提供按语言划分的参考文件,分析技能(assertion-quality、test-anti-patterns、test-gap-analysis、test-smell-detection、test-tagging)在分析 Python 代码前,必须先读取extensions/python.md获取框架相关的查找表,再进行判定。
调用链路如下(以 test-quality-auditor.agent.md 为入口):
- Agent 通过 marker 扫描检测语言与框架:Python 侧关注
pyproject.toml、setup.py、setup.cfg、pytest.ini、tox.ini、conftest.py、test_*.py、*_test.py; - 确认
extensions/python.md存在后,路由到各分析技能; - 每个技能(如 assertion-quality/SKILL.md、grade-tests/SKILL.md)在自己的工作流中强制要求先读取该扩展文件,再分类断言或打分。
因此,python.md实际上是整个仓库 Python 测试分析体系的“事实来源(source of truth)”,各技能通过它保持语言中立、避免用 .NET 或 JS 的框架术语去描述 pytest 套件。
二、能力标签:各检测能力的支持强度
python.md开头用一张能力标签表声明 Python 生态在各检测维度上的支持程度,下游技能据此决定是否启用对应流程:
| 能力 | 支持程度 |
|---|---|
| 测试发现(Test discovery) | 强 —— 约定驱动(test_*.py、*_test.py、Test*类) |
| 断言检测(Assertion detection) | 强 —— 裸assert、unittest方法、pytest.raises |
| Sleep/延迟检测 | 强 ——time.sleep、asyncio.sleep |
| 跳过/忽略检测 | 强 ——@pytest.mark.skip、unittest.skip |
| Setup/Teardown 检测 | 强 —— fixtures 与方法 |
标签支持(供test-tagging技能) | auto-edit——@pytest.mark.<tag>(pytest);unittest 无规范语法 |
其中“标签支持”是三类能力(auto-edit/report-only/convention-based)中的auto-edit:因为 pytest 有规范且可安全写入的 marker 语法,test-tagging技能可以直接在测试方法上插入@pytest.mark.<name>并修改文件;而 unittest 没有规范语法,只能产出报告(见下文“标签机制”一节)。这与 test-quality-auditor.agent.md 的能力矩阵中test-tagging | ✅ auto-edit(Python 行)完全一致。
三、测试文件识别(Test File Identification)
分析的第一步是发现哪些文件是测试文件。python.md给出的约定如下:
| 框架 | 测试文件约定 | 测试方法标记 |
|---|---|---|
| pytest | test_*.py或*_test.py | 以test_开头的函数;以Test开头(无__init__)的类及其中以test_开头的方法 |
| unittest | 任意模块(通常为test_*.py) | 继承unittest.TestCase且方法以test开头的类 |
两点实操细节值得注意:
- pytest 的类标记强调“以
Test开头且无__init__”,因为带构造器的类不会被 pytest 收集为测试类; - unittest 的方法前缀是
test(不含下划线),与 pytest 的test_不同,检测正则需区分对待。
在仓库实践中,code-testing-extensions/extensions/python.md 还补充了更广的测试形态(*.uts、test/*.sh、Djangoruntests.py/manage.py test),说明识别时应先探查仓库实际约定,而不是默认 pytest。
四、断言 API 对照表:pytest 与 unittest
python.md的核心是一张逐类别的断言对照表,这也是assertion-quality技能 Step 3 分类断言的依据(该技能定义了 12 个语言中立断言类别):
| 类别 | pytest | unittest |
|---|---|---|
| 相等 Equality | assert x == y | self.assertEqual(x, y) |
| 不相等 Inequality | assert x != y | self.assertNotEqual(x, y) |
| 布尔 Boolean | assert flag/assert not flag | self.assertTrue(flag)/self.assertFalse(flag) |
| None | assert x is None | self.assertIsNone(x)/self.assertIsNotNone(x) |
| 异常 Exception | with pytest.raises(SomeError) as exc_info: ... | with self.assertRaises(SomeError): ... |
| 类型 Type | assert isinstance(x, T) | self.assertIsInstance(x, T) |
| 同一性 Identity | assert x is y | self.assertIs(x, y) |
| 成员 Membership | assert item in collection | self.assertIn(item, collection) |
| 近似 Approximate | assert x == pytest.approx(y, rel=0.01) | self.assertAlmostEqual(x, y, places=2) |
| 字符串 String | assert sub in s/assert s.startswith(...) | self.assertIn(sub, s) |
| 跳过 Skip | pytest.skip("reason") | self.skipTest("reason") |
| 失败 Fail | pytest.fail("reason") | self.fail("reason") |
关键校准规则:不要误报裸assert
文档特别强调一条红线:裸assert是 pytest 的规范断言形式,会经 pytest 的断言重写(assertion rewriting)产生丰富的失败 diff。因此,在断言质量或反模式分析中,绝不能把裸assert当作“缺少框架 API”的异味。这条规则在 assertion-quality/SKILL.md 与 test-anti-patterns/SKILL.md 中都有对应条款(例如“pytest bareassertis the canonical assertion form, not a missing assertion library. Do NOT flag.”),可见下游技能对这份参考数据的遵守是显式约束。
第三方断言库
除两大框架外,文档还列出了常见的第三方断言库:assertpy、hamcrest(assert_that)、expects。分析器应对这些 API 同样给予识别。
五、Sleep/延迟模式(Sleep/Delay Patterns)
在检测测试脆弱性(flakiness)时,需要识别测试中的等待与延迟。python.md给出的模式清单:
| 模式 | 示例 |
|---|---|
| 同步 sleep | time.sleep(2) |
| 异步 sleep | await asyncio.sleep(1) |
| 循环等待 | while not condition: time.sleep(0.1) |
| Trio/anyio | await trio.sleep(...)、await anyio.sleep(...) |
在 test-anti-patterns/SKILL.md 中,time.sleep被列为High 严重级“不稳定性指标”(wall-clock sleeps 用于同步),但同时给出语境化豁免:若它出现在真正测试时序的集成测试中,则不构成反模式。
六、跳过/忽略注解(Skip/Ignore Annotations)
两种框架的跳过机制对应关系如下:
| 框架 | 注解 |
|---|---|
| pytest | @pytest.mark.skip(reason="...")、@pytest.mark.skipif(cond, reason="...")、@pytest.mark.xfail(reason="..."),以及内联pytest.skip("...") |
| unittest | @unittest.skip("reason")、@unittest.skipIf(cond, "reason")、@unittest.skipUnless(cond, "reason")、@unittest.expectedFailure |
检测这些注解的价值在于:跳过测试不等于断言为空;分析器应区分“主动跳过”与“无断言测试”。test-tagging技能还会把带有 flaky 注释的 skip 注解作为flaky特质(meta-tag)的启发式信号。
七、异常处理:惯用写法与异味判定
python.md给出了异常断言的两种惯用写法:
# pytest(推荐): with pytest.raises(ValueError, match=r"must be positive"): parse_amount(-5) # unittest: with self.assertRaises(ValueError): parse_amount(-5) # 需要检查异常对象时: with pytest.raises(ValueError) as exc_info: parse_amount(-5) assert "must be positive" in str(exc_info.value)三个要点:
- pytest 的
match=参数可以对异常消息做正则匹配,比 unittest 更精确; - 通过
as exc_info捕获异常上下文后,可对exc_info.value做二次断言; - 异味判定边界:仅当 try/except 之后没有断言、或异常被静默吞掉时,才把裸
try/except标记为“Exception Handling”异味。这与 test-anti-patterns/SKILL.md 中的“Swallowed exceptions(except:+pass)”以及“Assert in catch block only”两条 Critical 级反模式完全对应——正确的替代写法就是pytest.raises/assertRaises。
八、Mystery Guest:常见 Python 外部耦合模式
“Mystery Guest”指测试与外部环境(文件、数据库、网络、环境变量)隐式耦合。python.md给出了指示器对照表:
| 指示器 | 需要警惕的内容 |
|---|---|
| 文件系统 | open()、pathlib.Path(...).read_text()、os.path.exists、硬编码绝对路径 |
| 数据库 | 直接psycopg2/mysql.connector/sqlite3.connect到文件路径、SQLAlchemyengine 指向真实 DB URL |
| 网络 | requests.get/post、httpx.get/post、urllib.request.urlopen、裸socket |
| 环境 | os.getenv("X")(尤其无默认值时)、os.environ["X"] |
| 可接受 | io.StringIO/io.BytesIO、pytest fixturestmp_path/tmp_path_factory、monkeypatch.setenv、responses/httpx.MockTransport、pytest-mock、sqlite:memory: |
这张表是双刃剑:既列出了需要标记的外部依赖信号,也明确列出了可接受的替代品——分析器不应把tmp_path、monkeypatch、MockTransport误判为耦合。对应到 test-tagging/SKILL.md,使用真实数据库、HTTP 客户端、文件系统的测试会被归类为integration特质。
九、集成测试标记(Integration Test Markers)
识别集成测试的四类信号:
- 目录名:
tests/integration/、tests/e2e/、tests/acceptance/; - 命名:模块/类/函数名包含
Integration、E2E、EndToEnd、Acceptance; - Marker:
@pytest.mark.integration/@pytest.mark.e2e(项目自定义 marker,需在pytest.ini/pyproject.toml注册); - Conftest fixtures:启动容器/数据库的 fixture(
testcontainers、docker-composefixtures)。
在test-tagging的特质体系中,跨越进程/网络/持久化边界的测试标记为integration,走完整应用栈的标记为end-to-end,两者有明确区分。
十、Setup/Teardown 生命周期
| 框架 | Setup | Teardown |
|---|---|---|
| pytest | @pytest.fixture(任意 scope)、autouse=Truefixtures | fixture 内 yield 式 teardown 或request.addfinalizer |
| pytest(类级) | setup_method/setup_class | teardown_method/teardown_class |
| unittest | setUp/setUpClass/setUpModule | tearDown/tearDownClass/tearDownModule |
pytest 的 fixture 体系强调“按需付费(pay-as-you-go)”:一个 fixture 只有在测试请求它时才执行。因此文档在“语言特定校准说明”中明确:只被一个测试使用的 fixture 不算 General Fixture 异味。
十一、标签/特质机制(供 test-tagging 技能使用)
| 框架 | 标签机制 | 示例 |
|---|---|---|
| pytest | @pytest.mark.<name>(项目注册) | @pytest.mark.positive、@pytest.mark.boundary |
| unittest | 无内建机制 —— 使用类组织、属性或unittest.skipIf开关 | (仅报告;建议 pytest markers 或项目约定) |
pytest 的 marker 必须先注册,否则会产生PytestUnknownMarkWarning。python.md给出了pyproject.toml的标准注册写法:
[tool.pytest.ini_options] markers = [ "positive: verifies expected behavior under normal conditions", "negative: verifies handling of invalid input or error paths", "boundary: tests limits, thresholds, empty/null inputs", ]这与 test-tagging/SKILL.md 的实践衔接:该技能在 pytest(auto-edit)场景下为测试方法添加如下的标签,并在 Step 6 要求用仓库配置的 pytest 收集命令(如pytest --collect-only)验证编辑结果:
@pytest.mark.negative @pytest.mark.boundary def test_parse_none_input_raises_value_error(): ...由于 pytest 是 auto-edit 能力,test-tagging会直接修改文件;而 unittest 只能产出 Markdown 报告并建议项目约定——这正是能力标签表中“no canonical syntax in unittest”的落地体现。
十二、语言特定校准说明(Calibration Notes)
这是python.md中最需要“人工经验”的部分,直接决定分析器的误报率。逐条整理如下:
- 裸
assert是 pytest 惯用法—— 不要把它标记为“无断言”; - 快照测试(
syrupy、pytest-snapshot)用隐式快照比较替代了assert调用 —— 应视为合法断言; - 属性化测试(
hypothesis):被@given(...)装饰的函数即使看起来没有函数体也是真实测试 —— 断言内嵌在生成的输入循环中; - 异步测试(
pytest-asyncio、anyio):测试内部对协程调用缺少await会产生RuntimeWarning,且测试实际上“无断言化” —— 应标记为 Critical 反模式。这条与 test-anti-patterns/SKILL.md 中的“Missing await on async assertions”一致,也与 assertion-quality/SKILL.md 中“Async tests with unawaited assertions … treat as assertion-free”一致; - Doctests(通过
--doctest-modules调用):>>>块在用户将其纳入范围内时也应视为测试方法; - 参数化测试(
@pytest.mark.parametrize):不是底层函数的重复 —— 应视为合并后的形态(这也是 test-anti-patterns/SKILL.md 中“pytest/JUnit/xUnit parameterization 不视为重复”的依据); - 只被一个测试使用的 fixture 不算 General Fixture 异味:pytest fixtures 按需执行。
十三、在实际流水线中的综合应用
把上述参考数据放入 test-quality-auditor.agent.md 的“综合审计流水线”中看,Python 项目的审计路径是:
test-anti-patterns—— 用本文的 Sleep 模式、异常惯用法、Mystery Guest 指示器做严重级排序扫描;assertion-quality—— 用本文的断言 API 对照表做 12 类别分类与多样性度量(注意例外:异常测试天然低断言数、布尔断言检查具体属性不算平凡断言);test-gap-analysis—— 伪变异推理定位“改了生产代码但测试不会失败”的行为盲区,发现或断言语义不清时才调用test-analysis-extensions;test-tagging—— 用本文的标签机制为 pytest 套件添加@pytest.mark.<trait>并生成分布报告;覆盖率/CRAP 等步骤对 Python跳过并显式推荐coverage.py/pytest-cov、mutmut/cosmic-ray等原生工具。
输出报告时,grade-tests/SKILL.md 要求先读extensions/python.md再对断言打分;而 code-testing-extensions/extensions/python.md 则负责“写测试”一侧的运行命令(<prefix> pytest、python -m unittest discover、py_compile语法检查、环境前缀检测等)。两者与python.md形成“分析—评分—生成”的完整闭环。
结语
plugins/dotnet-test/skills/test-analysis-extensions/extensions/python.md以极紧凑的表格形式,沉淀了 pytest 与 unittest 在测试发现、断言、跳过、等待、耦合、生命周期与标签上的全部关键事实,并内嵌了多条“不要误报”的校准规则。对开发者而言,它是一份可直接照做的 Python 测试审查清单;对仓库内的分析技能而言,它是保证多语言分析语言中立、口径统一的权威参考数据。结合 test-analysis-extensions/SKILL.md 的说明——扩展文件是“数据”而非“教条”,它们告诉技能如何检测,而不是如何思考——使用时应始终结合被测代码的实际语义做出最终判断。
【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考