ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python自动化测试框架pytest实战:从unittest迁移到fixture与参数化

Python自动化测试框架pytest实战:从unittest迁移到fixture与参数化 1. 为什么一个写了三年 unittest 的测试工程师最后转投了 pytest先说背景。我负责的项目是一个典型的业务系统前后端分离后端接口一百多个核心流程涉及订单、支付、对账、权限几个模块。测试用例从最初的两三百条膨胀到后来的一千多条。用 unittest 写的时候最大的问题不是写不出用例而是用例越来越难维护。举一个最常见的例子每个测试类都要写setUp和tearDown初始化一个用户、初始化一个订单、清理数据库残留。类一多这套代码就要复制粘贴好几遍。想调整初始化逻辑得跑到每个测试类里去改漏改一个排查半天。这还没完——断言只能靠assertEqual、assertTrue这几个方法来回拼想做一个值落在某个范围两个列表的元素顺序无关但内容相同这种断言得自己写辅助函数写完了还得发给团队其他人用不然大家的写法都不统一。后来我花了两天把核心模块迁移到 pytest体验立刻不一样。pytest 的assert就是 Python 原生关键字失败时自动展开表达式细节不用记一堆assertXxx方法。fixture 机制把初始化和清理逻辑做成了可复用、可组合的依赖注入比setUp那套面向继承的设计灵活得多。最关键的是插件生态——并发执行、失败重试、生成 HTML 报告、按标签筛选用例全部是现成插件几乎不用自己造轮子。这篇文章就是围绕Python Pytest这套组合把我从选型到落地、再到排坑的完整过程写清楚。适合三类人看刚接触自动化测试、正在从 unittest 迁移、以及已经用 pytest 但总感觉用例一多就跑不稳的同学。文章里的所有代码都可以直接复制到你的项目里做验证操作步骤我尽量写到不用再查文档的程度。2. 环境准备与测试工程目录设计从第 0 天就定好规矩2.1 安装、解释器与虚拟环境的选择pytest 的安装没什么好说的一个 pip 命令的事pip install pytest但我要多说一句一定要在虚拟环境里做。尤其是同时维护多个项目的时候A 项目要用 pytest 6B 项目要用 pytest 8装在全局环境里迟早互相打架。我用的是 Python 自带的venv创建和激活都很直接python -m venv .venv source .venv/bin/activate # Windows 是 .venv\Scripts\activate pip install pytest确定安装成功pytest --version如果终端能正确输出版本号说明环境没问题。这一步看着简单但我在帮同事排查问题时遇到过好几回明明在终端跑pytest却提示找不到命令的情况——十有八九是虚拟环境没激活或者系统里装了多个 Python 解释器pip 装到了另一个版本上。用which python和pip --version对齐确认一下比较稳。2.2 推荐的项目目录结构测试代码不是随便建个test_xxx.py扔进去就行。项目一复杂目录结构直接决定用例的稳定性和可维护性。我整理了一个经过多次迭代后的稳定模板project_root/ ├── src/ # 被测项目源码 │ ├── order_service.py │ └── user_service.py ├── tests/ # 所有测试代码 │ ├── conftest.py # 全局 fixture、钩子函数 │ ├── test_order_service.py │ ├── test_user_service.py │ └── data/ # 测试数据文件 │ ├── order_data.json │ └── user_data.yaml ├── pytest.ini # pytest 核心配置 ├── requirements-dev.txt # 测试依赖 └── README.md几个容易踩坑的细节tests/目录下不用加__init__.py。pytest 有自己的 rootdir 和导入机制加了反而可能引发同名模块冲突。conftest.py是 pytest 的特殊文件名字不能改。放在tests/下整个目录的测试用例都能共享里面的 fixture 和钩子函数。pytest.ini放在项目根目录好处是无论从哪个子目录执行pytest都能按同一套配置跑不会出现换个目录跑结果不一样的怪事。2.3 pytest.ini 配置的几个关键项我目前的pytest.ini长这样[pytest] testpaths tests addopts -v -s --tbshort filterwarnings ignore::DeprecationWarning markers slow: 标记慢速用例 smoke: 冒烟测试用例逐个解释一下testpaths tests告诉 pytest 只去tests/目录找用例避免误扫到别的文件夹。addopts -v -s --tbshort-v让每个用例结果都显示出来-s允许print输出调试接口返回时非常有用--tbshort缩短报错堆栈页面干净不少。filterwarnings把第三方库的弃用警告统一屏蔽。注意这里我不建议写ignore::DeprecationWarning就万事大吉自己代码里的DeprecationWarning还是要让它显示该清理的警告早点清理。markers提前声明自定义标记避免用例运行时报unknown marker的 warning。这里有个细节pytest.ini必须是标准 ini 格式编码建议 UTF-8。如果里面有中文注释某些 Windows 环境下可能出现编码报错必要时把文件另存为 UTF-8 with BOM。3. fixture 机制这才是 pytest 真正赢过 unittest 的地方3.1 从 setUp/tearDown 到 fixture 的思维转变unittest 的初始化逻辑是继承式的写一个BaseTestCase在setUp里做公共初始化其他测试类继承它。问题是类 A 需要用户订单类 B 只需要用户类 C 需要用户订单支付记录。这时候要么硬着头皮建一堆中间基类要么在子类里重写setUp再调super()——层数一多谁调了谁、改了哪个字段会影响到哪条用例非常难追踪。pytest 的 fixture 是依赖注入式的每个 fixture 是一个独立的函数测试函数声明需要哪个 fixturepytest 就自动把它的返回值传进来。先看一个最小例子import pytest pytest.fixture def user_token(): # 模拟登录并获取 token token fake_token_123 return token def test_get_user_info(user_token): assert len(user_token) 0这里test_get_user_info的参数里写了user_tokenpytest 会去查找同名 fixture 并执行。这种写法和self.user_token的方式相比最大的好处是每个测试函数只依赖自己真正需要的东西不会因为继承链上的某个父类初始化了不需要的数据而拖慢速度。3.2 yield 形式的 fixture自带清理逻辑很多 fixture 不仅仅要准备数据还得在用例结束后做清理——比如删除临时文件、断开数据库连接、恢复被修改的配置。unittest 的做法是setUp配tearDown但 pytest 用yield一个关键字就搞定了import pytest import tempfile import os pytest.fixture def temp_env(): # 准备阶段 old_value os.getenv(APP_ENV) os.environ[APP_ENV] test tmp_dir tempfile.mkdtemp(prefixpytest_) print(f临时目录已创建: {tmp_dir}) yield tmp_dir # 在这之前的代码是准备之后的代码是清理 # 清理阶段 os.environ[APP_ENV] old_value or if os.path.exists(tmp_dir): for f in os.listdir(tmp_dir): os.remove(os.path.join(tmp_dir, f)) os.rmdir(tmp_dir) def test_use_temp_env(temp_env): assert pytest_ in temp_envyield之前的代码在用例开始前执行yield之后的代码在整个用例结束后执行。无论用例里抛出什么异常清理阶段的代码都会运行这一点比手写try...finally要省心很多。3.3 fixture 的作用域function、class、module、sessionfixture 默认在每个测试函数执行前都会重新跑一遍这叫scopefunction。但有些初始化开销很大比如连接数据库、加载大型配置文件每个用例都重跑就太浪费了。这时候可以指定作用域pytest.fixture(scopesession) def db_connection(): conn create_database_connection() yield conn conn.close() pytest.fixture(scopemodule) def order_data(): data load_order_data_from_file() yield data作用域的选择原则我总结成一句数据准备越快作用域越小数据准备越慢作用域越大但必须确认数据是只读的。scopesession的 fixture 在整个测试会话中只执行一次如果某个用例不小心修改了数据后面的用例全部受影响而且排查起来相当阴间。所以 session 级的 fixture 尽量只放连接类资源业务数据除非完全确定只读否则老老实实用 function 或者 module 级别。3.4 conftest.py 里的共享 fixture 与依赖注入多个测试文件共用的 fixture直接放在根目录下的conftest.py里。比如登录 token、公共的 API 客户端# tests/conftest.py import pytest from my_project.client import ApiClient pytest.fixture(scopesession) def api_client(): client ApiClient(base_urlhttp://test-server:8080) client.login(test_user, test_password) yield client client.close() pytest.fixture(scopefunction) def authed_client(api_client): # 基于 session 级的 client再给每个用例造一个新的带权限上下文 return api_client.with_new_context()看到没有fixture 之间也可以互相依赖——authed_client依赖api_client声明参数即可。这种fixture 套 fixture的组合能力是 unittest 的继承关系完全比不上的。我在实际项目中通常把 fixture 分成两层底层是资源连接类session 级上层是业务数据类function 级这样既快又稳。3.5 fixture 的按需使用autouse 与显式声明的取舍有一种场景很常见每条用例执行前都要做一些隐形初始化比如清空某个缓存、重置时间戳。每个函数都写参数太啰嗦这时可以用autouseTruepytest.fixture(autouseTrue) def _clean_cache(): cache.clear() yield注意我给这个 fixture 名字前加了_暗示这是一个不需要显式引用的自动 fixture。但autouse别滥用尤其不要在里面放重量级初始化——它会在每条用例前无条件执行范围比你想的大得多等用例数量上去了慢的原因可能就在这里。4. 参数化与断言机制用最少代码覆盖最多的边界场景4.1 parametrize 的基本用法与常见误区做接口测试时最常干的事是拿同一套测试逻辑、换不同的输入参数去验证。最原始的做法是写一个循环在循环里做断言但只要一个参数出错整个函数就停了后面的参数根本测不到。pytest 的pytest.mark.parametrize就是为这个场景设计的import pytest from my_project.calculator import calc_discount pytest.mark.parametrize( amount, level, expected, [ (100, normal, 100), (100, vip, 90), (100, svip, 80), (0, vip, 0), (500, vip, 430), ] ) def test_calc_discount(amount, level, expected): result calc_discount(amount, level) assert result expected每个参数组合都会被当成一条独立的用例来执行如果第三组数据算出来不对只有那一条标 FAIL其他照样 PASS排查范围精准很多。参数化有几个容易踩的坑:参数顺序必须和函数参数顺序一致parametrize第一个参数是参数名列表第二个是大列表里面每个元素是一个元组。看起来简单但嵌套层级一多括号和逗号经常数错。参数值如果是列表或者字典直接作为参数混在用例里可读性会比较差。建议单独抽象成变量名或者放到测试数据文件里用读取外部文件的方式去驱动。4.2 用外部文件做数据驱动我一直主张用例数量一多就把参数数据从代码里拆出来。我的做法是配合 yaml 文件# tests/data/discount_cases.yaml - amount: 100 level: normal expected: 100 - amount: 100 level: vip expected: 90 - amount: 500 level: vip expected: 430测试代码import pytest import yaml def load_discount_cases(): with open(tests/data/discount_cases.yaml, encodingutf-8) as f: return yaml.safe_load(f) pytest.mark.parametrize(case, load_discount_cases()) def test_calc_discount_from_yaml(case): result calc_discount(case[amount], case[level]) assert result case[expected]这么做的直接好处是业务同事也能改测试数据不用碰代码。提需求的时候直接改 yaml跑一遍就知道结果省掉很多沟通成本。需要提醒的是load_discount_cases()会在收集阶段执行如果文件路径写错或者格式有毒会直接让整个测试会话 crash所以在数据文件上做个简单的 schema 校验会稳妥不少。4.3 pytest 的 assert 增强失败时自动展开表达式pytest 最吸引我的特性之一就是把 Python 自带的assert变得会说话。看个例子def test_user_age(): user {name: 张三, age: 18} assert user[age] 20运行输出会直接告诉我们 assert user[age] 20 E AssertionError: assert 18 20 E where 18 user[age]它不只告诉你AssertionError还会把表达式里每个子表达式的值都展开。这种细节在排查复杂断言时非常实用——不需要重新打印变量失败信息已经写清楚了。至于复杂数据结构的断言我一般分两种情况处理简单精确匹配直接assert a b。需要忽略某些字段用in和not in组合而不是硬撑一个巨型断言表达式。4.4 浮点数比较与近似断言接口返回经常带price这类浮点字段直接assert result 19.99看着没问题实际可能因为换算导致 19.989999...断言失败。我还见过不少同事在这种地方浪费时间最后发现根本是浮点精度问题。pytest 给了一个实用插件pytest-approx用法如下import pytest def test_price_approx(): price 19.989999999999998 assert price pytest.approx(19.99)更常见到的是内置的pytest.approx它能设置相对误差也可以指定abs绝对误差assert price pytest.approx(19.99, abs0.01)建议团队约定凡是涉及浮点结果统一用pytest.approx禁止直接。这条约定能让很多莫名其妙的偶发失败直接消失。5. 测试数据与用例隔离让每个用例从头开始保持干净5.1 为什么用例之间会互相污染自动化测试最让人头疼的问题之一就是单独跑全绿一起跑就挂。99% 的情况是共享数据被前面的用例改了。比如用例 A 往数据库里插入了一条订单用例 B 查订单总数时把这条也算进去了结果断言失败。这种问题有个专门的名字——测试数据污染。我处理这个问题的原则很简单分三层每个用例尽量自造数据不依赖历史残留数据。共享资源数据库、缓存在用例执行前重置。必要的情况下让用例之间串行并在关键节点清理。5.2 用 tmp_path 处理文件型数据如果你要测试的是文件读写逻辑千万不要在项目目录里创建临时文件测试完又删。pytest 内置了一个tmp_pathfixture它会在测试会话期间创建一个临时目录用例结束后自动清理def test_save_order(tmp_path): order_file tmp_path / order_001.json save_order(order_file, {order_id: 1}) assert order_file.exists() assert order_file.read_text(encodingutf-8) {order_id: 1}tmp_path是pathlib.Path对象直接能用/拼接路径。如果是函数内临时文件用tmp_path_factory可以从更底层控制生命周期。5.3 数据库场景使用事务回滚代替造数据做数据库相关测试时最痛苦的环节是数据库里残留了一堆测试数据。用 pytest 配合数据库事务回滚能做到用例结束数据自动还原import pytest pytest.fixture def db_session(): # 打开一个事务 session create_session() yield session # 回滚不 commit session.rollback() session.close() def test_create_order(db_session): db_session.add(Order(id1, amount100)) db_session.flush() # 这里查询能查到但用例结束后 rollback数据不会留在库里 order db_session.query(Order).filter_by(id1).first() assert order.amount 100核心逻辑永远不要在测试里 commit只做flush保证同一事务内可见结束时统一rollback。这比每个用例单独 delete 残留数据要可靠得多——删除语句漏写一条就是一次半夜被测试报警吵醒的代价。5.4 环境变量的注入与还原接口测试经常需要切换配置比如把环境切到测试环境。这里我会用monkeypatchfixture 来改环境变量而不是直接os.environ[...] ...def test_with_env(monkeypatch): monkeypatch.setenv(APP_ENV, test) assert get_current_env() testmonkeypatch.setenv的好处是无论测试成功还是失败用例结束后环境变量都会自动恢复原值不用 manually 写os.environ回滚逻辑。同理monkeypatch.setattr可以临时修改对象的属性或方法是打桩/替换依赖的利器def test_mock_time(monkeypatch): def fake_today(): return 2026-01-01 monkeypatch.setattr(my_project.utils.today, fake_today) assert get_today_str() 2026-01-016. mark、skip 与超时控制精准管理你的测试集合6.1 用 mark 标记用例冒烟、慢速、前后端分层我经常看到有人说pytest 跑起来太慢了但仔细一问经常是他们把所有用例不分轻重地一股脑全跑。测试用例也应该像代码一样分级冒烟级smoke必须快回归级可以慢慢速的甚至可以定期跑。在 pytest 里这个能力来自markimport pytest pytest.mark.smoke def test_health_check(): assert api.health() ok pytest.mark.slow def test_full_report_generation(): # 耗时数十秒的用例 report generate_monthly_report() assert report.total 0然后就可以用参数过滤执行pytest -m smoke # 只跑冒烟用例 pytest -m not slow # 排除慢速用例 pytest -m smoke and not slow加了这种分级之后我通常的做法是每次提交代码先跑pytest -m smoke几十秒内出结果晚上再让 CI 跑全量回归。体感好了不止一点点。6.2 skip 与 skipif不是所有用例都适合在当前环境跑有些用例在特定环境才有效。比如只在 Windows 上跑的代码或者依赖某个可选第三方服务的用例。硬要让它跑只会收获一堆 xfail所以合理跳过才是正解import sys import pytest import my_project pytest.mark.skip(reason当前版本功能未上线暂跳过) def test_new_feature(): pass pytest.mark.skipif( sys.platform ! win32, reason该功能只在 Windows 下生效 ) def test_windows_only(): pass另外xfail也值得单独说一句。它适合已知 bug 但还没修复的场景标记xfail后如果用例按预期失败pytest 会显示 xfailed不算 FAIL一旦它意外通过了你会看到一个 XPASS 警告——这说明 bug 已经修复可以把xfail摘掉了。6.3 超时控制给慢用例上保险丝测试最怕卡死。比如某个接口调用了下游服务下游响应迟迟不回整个测试卡在那里后面的用例全部排队等待。解决办法是加超时机制。推荐使用pytest-timeout插件pip install pytest-timeout用法有三种常见方式全局配置[pytest] timeout 30单条用例标记pytest.mark.timeout(5) def test_slow_response(): assert api.call_with_timeout() is not None运行时传参pytest --timeout30这个插件还有一个我认为很实用的模式--timeout-methodthread比默认的signal方式更适合在多线程环境下使用不容易误杀主线程。遇到间歇性超时的问题把超时时间放宽一点再配合下面要讲的重试机制基本能覆盖掉大部分网络抖动的情况。6.4 失败自动重试pytest-rerunfailures网络和外部环境的不稳定是测试工程师必须承认的现实。某个接口偶发超时 2 秒不代表功能真的挂了但如果测试直接失败就会造成大量的误报开销。我的做法是用pytest-rerunfailures插件pip install pytest-rerunfailures pytest --reruns 2 --reruns-delay 3这个命令表示失败的用例自动重跑 2 次每次间隔 3 秒。重试仍失败的才算真失败。需要注意重试机制不能用得太宽否则会掩盖真正的 bug。我的策略是只在接口层测试设置重试单元测试和纯逻辑测试一律不重试。因为接口层的偶发失败来源确实是网络而单元测试应该具备确定性。7. 报告、Hook 与 CI 集成让测试结果真正有用7.1 HTML 报告pytest-html 与 Allure 的取舍跑测试只是第一步让结果容易看才是自动化测试能长期运转的关键。我先说最常用的pytest-htmlpip install pytest-html pytest --htmlreport.html --self-contained-html--self-contained-html会把 CSS、JS 全塞进一个 HTML 文件里方便直接用浏览器打开也可以随手发给同事。不过它的报告比较朴素只是把控制台的 PASS/FAIL 变成表格。如果要更美观、更丰富的报告可以用 Allure。Allure 本身是 Java 工具链但 pytest 通过allure-pytest插件集成可以自动抓取用例步骤、附件、参数等。Allure 报告的优点是好看到可以发给领导看缺点是要多装一个 Allure 命令行工具而且如果团队没人维护容易变成报告比测试还难管。我的建议团队规模小、看结果的人少用 pytest-html 省心团队规模大、需要长期展示结果的时候再上 Allure。7.2 conftest.py 中的 Hook让测试反馈更符合团队习惯pytest 的钩子hook机制允许我们在测试生命周期的关键节点插入自定义逻辑。我常用的是这两个# tests/conftest.py import json import pytest def pytest_addoption(parser): 自定义命令行参数比如切换环境地址 parser.addoption(--env, actionstore, defaulttest, help指定测试环境: test / staging / prod) pytest.fixture(scopesession) def env(request): return request.config.getoption(--env) def pytest_collection_modifyitems(items): 收集用例后自动排序smoke 用例优先执行便于快速反馈 items.sort(keylambda item: 0 if item.get_closest_marker(smoke) else 1)pytest_addoption让我们在命令行里可以用pytest --envstaging指定要跑的环境比改配置文件方便得多。pytest_collection_modifyitems则可以把冒烟用例排到前面一旦某个冒烟用例失败能第一时间暴露在最显眼的位置。另一个我常用的 hook 是pytest_terminal_summary用来在收尾时输出一些自定义统计信息比如测试期间创建了多少条临时数据是否需要清理测试账号。这种细节能让测试报告变成一线工程师的好伙伴而不只是一堆 PASS/FAIL 的数字。7.3 与 CI 的集成思路到了 CI 阶段我推荐的做法是在 GitHub Actions 或者 Jenkins 的流水线里让每次提交都跑冒烟用例每晚跑全量并上传 HTML 报告。伪代码大概是pip install -r requirements-dev.txt pytest -m smoke --htmlsmoke_report.html --self-contained-html如果跑的是全量用例记得加上并发参数提升速度见第 8 节pytest -n 4 --distloadfile --htmlfull_report.html --self-contained-html关于 CI我特别想强调一个经验不要在 CI 上跑所有测试然后才去看报告应该把测试分成多层提交 CI 跑快速集合并到主干才跑重型集。否则一旦用例数量上千一个全量回归跑 40 分钟开发流程直接变成提交一分钟排队四十分钟。8. 踩坑实录我在实际项目中踩过的几个典型的坑与排查过程8.1 rootdir 与相对路径导致用例在不同目录执行结果不一致某个模块的测试代码里用了相对路径读配置文件config open(config.ini)本地在项目根目录跑pytest时一切正常CI 里换成pytest tests/test_config.py就报文件找不到。查了半天原因是 pytest 的rootdir概念所有相对路径都是相对于 rootdir通常是 pytest.ini 所在的目录而不是当前跑命令的目录。解决办法有两个选一个就行用pathlib.Path(__file__).parent定位文件from pathlib import Path config_path Path(__file__).parent.parent / config.ini或者在conftest.py里把它转成绝对路径的 fixtureimport pytest from pathlib import Path pytest.fixture(scopesession) def project_root(): return Path(__file__).parent.parent这个坑的真正解法是不要在测试代码里写裸的相对路径或绝对路径一切基于当前文件位置计算。8.2 全局配置被 session 级 fixture 污染导致单测不隔离有段时间系统所有用例都开始偶发失败单独跑每个文件都是绿的。排查过程中我发现罪魁祸首是一个scopesession的 fixture 修改了全局配置对象——某个用例改了配置项后面的所有用例都在新配置下执行。解决方案其实上面第 5 节已经提到session级 fixture 里千万别改可变全局对象。如果一定要修改可以用monkeypatch在 function 级里做保证每个用例结束时恢复原状。这条经验让我后来写 fixture 时形成了条件反射凡是会修改状态的 fixture一律默认 function 作用域。8.3 并发执行xdist时小心共享文件和端口冲突用例多起来后单线程跑确实慢于是我引入了pytest-xdistpip install pytest-xdist pytest -n 4速度提升非常明显但接踵而来的是各种稀奇古怪的失败。后来发现两个根源一是多个 worker 进程同时写同一个临时文件/数据库互相覆盖二是每个 worker 进程共享同一组端口导致接口测试串台。解决办法是涉及文件写入的用例必须用tmp_path因为每个 worker 的tmp_path是隔离的。涉及端口绑定的用例通过pytest_addoption传入一个端口偏移量或者随机选端口。如果实在没法隔离就先限制这类用例串行执行pytest -n 4 --distloadscope--distloadscope会尽量让同一模块下的用例分到同一个 worker有效减少互相干扰。8.4 断言失败信息不直观排查成本高还有一个容易被忽略的细节断言信息写得不好真的能让人 debug 一小时。比如assert response.status_code 200一旦失败只告诉你 500 不等于 200但你想知道响应体里具体是什么错误。建议写成assert response.status_code 200, f接口返回异常, 状态码{response.status_code}, 响应体{response.text}这种断言自定义失败信息的组合在接口测试里极其常用能省掉很多日志翻找时间。8.5 测试代码里不要写过于复杂的业务逻辑最后这一点可能听起来像废话但我见过不少同事在测试里复制了一大段业务代码来校验被测函数的输出。这样做非常危险——一旦被测函数和测试里的预期逻辑犯了同一个错测试永远全绿bug 被完美隐藏。测试里的期望值应该尽量来自人工指定的、独立于实现的数值或规则而不是从业务代码里现算出来。如果你发现自己为了校验一个函数写了比它本身还复杂的影子实现那就要停下来想想——这个测试到底在验证什么。9. 最后再说点实际经验pytest 的上手曲线不算陡但真正用好它靠的是一套规则感fixture 怎么分层、数据怎么隔离、用例怎么按场景分级、断言怎么写得可读可排。这些规则不需要一开始就全部定完可以随着项目规模的增长逐步收紧。我现在的习惯是每个新项目一开始就建好tests/目录、配好pytest.ini、写好基础的conftest.py哪怕只有 10 条用例也按规范跑。因为等用例膨胀到 500 条时再重塑规范付出的成本是十倍百倍。如果你正在用 unittest 觉得别扭或者用 pytest 却总在排坑可以对照这篇文章的目录结构、fixture 分层、参数化、数据隔离、CI 集成这几个维度一条条检查自己的项目。别急着一次性把全部东西都加上先捡对你当下最痛的那个点改起来。改完之后你会发现自动化测试不再是一个运行完就没人看的黑盒子而是一个能让整个团队对代码质量越来越有信心的基础设施。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进