资讯详情 Pytest入门:从Unittest痛点走向优雅自动化测试
📅 2026/10/11 6:26:11
1. 为什么是Pytest从Unittest的痛点说起1.1 一个让测试代码变优雅的契机我刚接触自动化测试那会儿用的还是Python自带的unittest写一个用例要先继承TestCase再写setUp和tearDown方法名必须以test开头不说断言还得记着assertEqual、assertTrue、assertIn这一大堆API。最崩溃的是我只想做一个简单的数据校验却要先写上十几行模板代码。当时我就在想测试代码本身就够枯燥了为什么还要被框架的条条框框束缚得这么累后来在一次项目重构中我偶然尝试了pytest第一次运行pytest命令看到终端里那些绿色的点号和一个不落的passed时我整个人是有点震惊的。不需要写类不需要继承一个普通的函数加上assert断言就能成为一个测试用例。更关键的是它的fixture机制让我彻底告别了setUp和tearDown那套繁琐的写法。那一刻我才真正理解什么叫更优雅的测试——优雅不是花哨而是让测试代码回归本质写条件、做断言、看结果。这篇文章不是什么理论堆砌而是我从实际项目中趟出来的经验总结。无论你是刚接触自动化测试的新人还是被unittest折磨过、想换框架的老手这篇Pytest入门都能让你在最短时间内把它用起来并且用到项目里真正能落地的地方。1.2 Pytest与Unittest的对比它赢在哪在我给团队做技术选型时经常被问到pytest到底比unittest好在哪里。我用一个很直观的对比来说明同样的一个测试需求unittest可能需要写一个类、三个方法、两种断言API而pytest只需要一个函数、一条assert、一句命令行。这种简洁不是靠牺牲功能换来的而是pytest的设计哲学决定的——它把测试用例收敛成了函数 断言的最小单元把那些重复的初始化、清理工作统一交给fixture去管理。除了写起来省事pytest在运行效率上也占优势。它的断言内省机制会在失败的时候自动打印出两侧表达式的值比如assert a b失败了你不用自己拼日志pytest会告诉你左边是多少、右边是多少哪个元素不匹配。这个能力在日常排错中特别省时间尤其是对比长字典、大列表的时候一眼就能定位问题在哪。另外unittest的用例发现机制相对死板而pytest支持自定义匹配规则、按节点选择、按标记筛选想跑哪组就跑哪组灵活度完全不在一个层级上。还有一点容易被忽略pytest有极其丰富的插件生态。覆盖率、报告、并发、mock、重试几乎你能想到的测试痛点都有对应的插件而且都是社区验证过的方案。它不是一个人在战斗而是带着整个生态在为你服务。这也是我在多个项目中最终统一选择pytest的根本原因。1.3 Pytest的核心设计哲学要说pytest最核心的设计理念我的理解就一句话把测试写成人能看懂的样子而不是机器要求的样子。它不强制你使用特定结构而是通过约定优于配置的方式给予你最大的自由。测试文件叫什么名字、用例函数怎么命名、fixture怎么共享这些都有默认约定但你可以随时覆盖。这种设计的好处是团队成员上手极快。新人不需要先学一堆框架概念只要会写Python函数、会用assert就能写出有效的测试。而随着项目变大pytest又会慢慢引导你去使用fixture、参数化、conftest.py这些高级特性整个过程是渐进的不是一上来就给你一大堆概念。用个不恰当的比喻unittest像是一张需要填写的表格pytest更像一张白纸——它给你工具和规则但把创造力留给你自己。2. 环境准备与第一个测试用例2.1 安装Pytest两条命令的事安装pytest非常简单使用pip即可。如果你用的是虚拟环境我强烈建议每个项目都建虚拟环境在项目根目录下执行pip install pytest如果你不确定当前环境的版本可以查看一下pytest --version我目前常用的版本是8.x如果你是在老项目上安装建议至少使用7.0以上因为6.x及更早版本在部分断言内省和fixture功能上有些小问题越新版本对类型注解的支持也越好。这里说一个我踩过的坑在Windows环境下如果之前装过别的测试框架可能会把pytest命令指向了其他程序解决办法是用python -m pytest来运行它能确保调用的是当前Python解释器对应的pytest模块。另外建议顺手安装两个非常基础的插件一个是pytest-cov统计覆盖率一个是pytest-html生成测试报告。后面的章节我会专门讲这些插件怎么配置现在安装阶段就先带上pip install pytest-cov pytest-html2.2 编写并运行第一个测试传统的你好世界在测试领域就是写一个最简单的判断函数。我们新建一个文件命名为test_demo.py注意pytest默认会匹配test_*.py或*_test.py这样的文件命名规则# test_demo.py def test_addition(): result 1 1 assert result 2 def test_string(): name pytest assert py in name然后在终端中运行pytest你会看到类似如下的输出collected 2 items test_demo.py .. [100%] 2 passed in 0.02s那两个点号就代表两个用例通过。如果其中一个失败了比如我把断言改成assert result 3再运行一次pytest会显示详细的失败信息包括具体的行号、表达式内容、左右值对比。这就是我前面说的断言内省能力——它让失败信息不再是冷冰冰的AssertionError而是直接告诉你哪里不对、差了多少。这里有个命名上的细节必须强调测试函数必须以test开头测试文件也必须以test开头或结尾匹配。如果你把函数命名为check_addition()pytest会直接忽略它而且不会给你任何提示。这个规则看似简单却是新人最容易踩的坑之一。2.3 断言的艺术看懂pytest的失败信息写pytest用例核心就是写断言。Python原生assert语句是pytest的首选断言方式它不需要你记assertEqual这种API而是直接用、!、in、is等Python比较运算符。我们看一个稍微复杂点的例子def test_dict_compare(): expected {name: pytest, version: 8, tags: [test, framework]} actual {name: pytest, version: 7, tags: [test, framework]} assert actual expected运行失败后pytest会非常明确地指出字典中version字段不一致左边是7、右边是8。如果你对比的是长列表它还会标出第一个不相等的元素索引。这种精细的失败信息在手工拼日志的时代是完全不敢想的。我还想分享一个关于断言的小技巧不要在一个用例里塞太多断言一个用例最好只验证一个核心行为。原因很简单——当第一个断言失败时后续断言都不执行了你只能看到第一个问题而如果把每个行为拆成独立用例一次运行就能暴露所有问题。此外pytest还支持在断言后面加自定义说明信息比如assert x y, x和y不一致这在维护大型测试集时特别有用失败信息可读性会大大提升。3. FixturePytest的灵魂3.1 从setUp到Fixture数据准备的进化如果你用过unittest一定写过这样的代码在setUp方法里做数据初始化在tearDown里做清理。问题是一个类里的所有用例共享同一套初始化和清理逻辑不同用例如果需要不同的数据准备你得写多个类或者在一个setUp里塞满条件分支代码越来越难维护。pytest的fixture把这件事彻底颠覆了。fixture就是一个用pytest.fixture装饰的函数它可以返回数据也可以只做前置/后置操作。最妙的是fixture是按需注入的——一个测试函数参数列表里写了哪个fixture名称它就会自动执行那个fixture不需要继承、不需要全局定义。看个例子import pytest pytest.fixture def user_data(): # 模拟一个用户数据对象 return {id: 1, name: zhangsan, age: 20} def test_user_name(user_data): assert user_data[name] zhangsan def test_user_age(user_data): assert user_data[age] 18user_data这个fixture函数被两个测试用例同时使用每个用例都会执行一次fixture拿到数据。如果有清理逻辑要写fixture可以用yield语句实现yield之前的代码在用例开始前执行yield之后的代码在用例结束后执行。pytest.fixture def db_connection(): conn create_connection() yield conn conn.close()这比tearDown强在哪第一它粒度可控不同fixture可以组合使用一个用例想用哪个就写哪个第二它作用域可控不仅可以函数级执行还能做到模块级、类级、会话级第三它可以依赖其他fixture形成清晰的依赖链。这才是优雅测试的真正底色。3.2 conftest.py与共享Fixture当一个fixture要被多个测试文件共享时把它放到conftest.py里即可。conftest.py是pytest的特殊文件它不需要被任何地方导入pytest会自动识别并把其中定义的fixture提供给同目录及其子目录下的所有测试用例。我习惯的工程结构是这样的project/ ├── conftest.py # 全局共享fixture ├── tests/ │ ├── conftest.py # 该目录共享fixture │ ├── test_api.py │ └── test_utils.py顶层conftest放最通用的fixture比如测试环境的初始化配置、公共的API客户端。子目录的conftest放局部专用的数据。这样分层的设计既清晰又高效。需要注意conftest.py中定义的fixture不会被子目录之外的用例感知到它的作用范围就是所在目录及以下想全局共享就放到根目录。还有一个高频问题同一个fixture在conftest和测试文件中都定义了怎么办答案是就近优先pytest会优先使用距离测试文件最近的fixture定义。这个规则在团队协作时偶尔会引起困惑但只要你遵循不要把同名fixture在不同层级重复定义这条原则基本不会踩坑。3.3 参数化一份测试逻辑跑N组数据在测试中最烦人的工作之一就是同样的断言、不同的输入。如果复制粘贴代码会变成一坨屎山如果用循环失败了很难定位是哪一组数据出了问题。pytest的pytest.mark.parametrize优雅地解决了这个痛点。import pytest pytest.mark.parametrize(num, expected, [ (1, 2), (2, 4), (10, 20), (0, 0), (-3, -6), ]) def test_double(num, expected): assert num * 2 expected运行后你会发现pytest把每一组参数都当作一个独立的测试用例单独显示ID和结果。如果第三组失败了输出的节点ID会明确告诉你test_demo.py::test_double[10-20]这个用例出了问题一眼可查。参数化还可以叠加使用形成笛卡尔积。更实用的场景是用参数化把测试数据和期望结果直接写在用例上方让测试变成了一个可读性极强的数据表。这种方式特别适合接口测试一组URL、一组参数、一组预期状态码几十个接口用例用一个小列表就搞定了。后面我会在实战部分专门演示这个场景。4. 测试标记与选择性运行4.1 用Marker给用例打标签pytest有一个很强大的功能叫做marker标记它允许你给不同的用例贴上不同的标签然后根据标签选择性的运行。用法非常简单import pytest pytest.mark.smoke def test_login(): assert login(admin, 123456) pytest.mark.slow def test_full_report(): assert generate_report()运行的时候只跑冒烟测试pytest -m smoke不跑慢速测试pytest -m not slow这个能力在日常回归中尤其重要。一个项目跑全量测试可能要半小时但你每次提交代码都跑全量根本不现实。更合理的策略是全量测试每天固定时间跑一次每次提交代码只跑冒烟和跟本次改动相关的用例。这种分层分级测试策略对CI流水线来说几乎是必备的。我一般在项目里定义这么几个通用标记smoke冒烟、slow慢速、api接口测试、uiUI测试、regression回归。还有一个实用的地方pytest允许在pytest.ini文件里注册标记并添加说明避免拼写错误。记住如果使用了未注册的标记pytest会给出warning把标记都注册一下团队协作时方案更规范。4.2 条件跳过让测试更智能有的测试用例在某些环境上根本无法运行比如Windows下没法测Linux特性或者依赖某个外部服务而服务未启动。这时候硬跑只会得到一堆失败掩盖真实问题。pytest提供了skip和skipif两种跳过机制import pytest import sys pytest.mark.skip(reason功能尚未实现) def test_not_ready(): ... pytest.mark.skipif(sys.platform win32, reason仅支持Linux) def test_linux_only(): ...还有pytest.mark.xfail这个特别适合处理已知问题但暂时不修的场景。标记为xfail的用例如果失败了不会算作失败而是显示为预期失败万一它居然通过了pytest会提示XPASS这时候你反而要注意——是不是问题悄悄修好了该做调整了。这个机制对于团队管理技术债非常有用。4.3 按目录和节点运行灵活度拉满除了-m按标签筛选pytest还支持按路径和节点运行用例。单个文件直接指定文件路径单个用例用::连接pytest tests/test_api.py pytest tests/test_api.py::test_loginparametrize产生的用例节点ID里包含参数值你甚至可以精确到某组参数。这种从粗到细的执行控制在处理失败用例复现问题时特别高效。我通常的排查流程是全量跑一遍收集失败列表然后逐个用pytest 文件::用例 --pdb的方式进入调试模式定位问题。等下--pdb这个参数就是用例失败时自动进入Python调试器配合断言信息和断点能把定位问题的速度提升好几倍。这个技巧强烈建议你记住排查问题的时候真的会救你命。5. 断言与异常测试的进阶玩法5.1 不仅仅是Equalpytest的内建断言工具日常测试中我们更多需要的是验证某个行为是否符合预期而不仅仅是比较两个对象是否相等。pytest在Python原生assert之上还提供了pytest.approx用于浮点数比较、pytest.raises用于断言抛出异常、pytest.warns用于断言警告信息。浮点数比较是大坑因为二进制表示导致0.1 0.2 0.3在计算机里是False。遇到涉及浮点数计算的测试直接用断言基本必挂。正确的写法是import pytest def test_float(): assert (0.1 0.2) pytest.approx(0.3)pytest.approx内部会使用相对容差和绝对容差来判断两个浮点数是否足够接近。这个细节真是太重要了我在做数值计算相关的测试时几乎每个涉及浮点的断言都离不开它。5.2 用pytest.raises测试异常行为有些时候我们的测试目标恰恰是验证代码在非法输入下会抛出指定异常。比如一个除法函数除以零应该抛ZeroDivisionError一个接口校验函数传参缺字段应该抛ValueError。这时用try...except写会很啰嗦pytest.raises就非常雅致import pytest def divide(a, b): return a / b def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): divide(1, 0)pytest.raises还可以捕获异常信息进一步验证异常中的细节内容。比如异常消息里应该包含参数不能为空这样的提示我们就可以match参数来正则匹配def test_error_message(): with pytest.raises(ValueError, match参数不能为空): validate_name()这种断言方式把异常即行为的测试理念贯彻得很彻底。记住一个原则异常路径和正常路径一样需要测试覆盖。很多团队测试覆盖率高但异常处理分支几乎没测上线后隐患就暴露在那些没人动过的异常逻辑上。5.3 字典和集合比较细节中的魔鬼接口测试中返回的常常是JSON对象也就是Python字典。字典比较时有个细节要特别注意字典顺序在Python 3.7之后虽然保持了插入顺序但字典的比较是不区分顺序的只要键值对相同就相等。所以{a: 1, b: 2}和{b: 2, a: 1}是相等的这符合业务预期。但如果你需要校验字典的键顺序就必须显式比较list(d.keys())。另一个常见需求是只校验部分字段比如接口返回了一堆数据但我只关心其中几个字段是否正确就可以从返回结果中提取出关心的字段组成新字典再比较。这种局部断言的思想比直接比对整个返回体要靠谱得多因为全量比对过于脆弱——上游数据稍作调整你的用例就得跟着改。嵌套结构的比较在接口测试中也很常见。字典里套列表、列表里套字典出错时pytest的信息能精确到最深层级的不匹配元素这在内省机制的支持下就是它最大的优势。遇到特别复杂的结构我一般直接用json.dumps(..., indent2, sort_keysTrue)先把两个对象格式化输出到日志里肉眼对比起来更方便。这种土办法在某些时候比看断言信息还直观。6. 插件生态让测试进入工业化阶段6.1 pytest-html生成漂亮的测试报告测试跑完没报告等于白测。虽然命令行输出已经足够观察但在团队协作和CI回执中一份结构清晰的HTML报告是刚需。pytest-html插件生成报告的方式极其简单pytest --htmlreport.html --self-contained-html--self-contained-html参数会把CSS和JS都内嵌到文件中这样报告可以单独作为附件发送不依赖网络也能在浏览器正常渲染。报告里会展示每个用例的执行时间、状态、失败原因还有按模块和标记分类的汇总信息。我在项目中通常把它作为CI流程的默认产物每次流水线结束都能自动生成并归档方便回溯每一次构建的测试情况。也有团队选择Allure报告相比pytest-htmlAllure的颜值和交互确实更胜一筹但需要额外部署Allure服务端环境配置成本更高。我的建议是个人项目或小团队先用pytest-html轻量、够用如果公司有专门的测试基础设施再考虑引入Allure全家桶。6.2 pytest-xdist把单线程的测试跑成并行默认情况下pytest是单线程串行跑用例的。当用例数量上到几千条执行时间就会变得难以忍受。pytest-xdist插件让并行测试变得非常简单pytest -n auto-n auto表示自动根据CPU核心数决定并行进程数。指定具体进程数比如4核机器跑4个进程直接pytest -n 4。这里要注意一个并行测试的经典陷阱不是所有测试都适合并行。如果多个用例操作同一个测试数据库、同一个文件、同一个外部服务状态并行反而会引入数据竞争和不稳定因素。我遇到过的最严重情况是并行后偶发生成订单号重复导致几个用例不稳定地失败排查了很久才发现是共享的计数器变量被并发修改了。正确做法是先把用例分类无状态的用例放一个目录放心并行有状态或依赖外部资源的用例加标记serial然后利用-m serial和-m not serial分开跑。这一套下来通常能在不牺牲稳定性的情况下大幅缩短全量测试时间。在我的一个项目里4000多条用例从40分钟压缩到了11分钟效果立竿见影。6.3 pytest-mock与其他常用助手如果你在测试中需要mock掉外部接口或依赖pytest-mock插件提供了一种更简洁的写法。它内置了一个叫mocker的fixture无需手动from unittest import mock和with mock.patch(...)连连看def test_user_service(mocker): mock_get mocker.patch(myapp.utils.request_get, return_value{code: 0}) result user_service.fetch_info(1) assert result[code] 0 mock_get.assert_called_once_with(/user/info/1)对比unittest里mock.patch的上下文管理器写法mocker接口明显舒服得多。它还能自动完成mock对象的清理不会出现用例之间mock相互污染的问题。这个插件几乎是我每个项目的标配。还有其他几个高频实用插件值得一提pytest-cov统计代码覆盖率并支持阈值校验pytest-repeat重复执行用例以排查偶发问题pytest-timeout给用例加超时限制防止某个用例卡死拖垮整个测试套件。插件生态是pytest区别于其他框架的巨大优势善用它们能让测试体系直接进入工业化阶段。7. 实战案例一个API接口测试从0到17.1 项目结构设计理论讲再多不如一个完整的案例来得直观。我带大家做一个简单的用户信息接口测试假设后端提供了一个RESTful APIGET /api/users/{id} # 获取用户信息 POST /api/users # 创建用户项目结构这样安排api_test/ ├── conftest.py # 全局fixture ├── config.py # 测试环境配置 ├── utils/ │ ├── __init__.py │ └── api_client.py # 封装请求 ├── testcases/ │ ├── test_user_api.py │ └── test_user_create.py └── requirements.txt在conftest.py中定义几个全局fixture比如base_url、api_client。utils/api_client.py封装一下requests提供一个简单的API调用方法。config.py可以定义今天用的环境地址比如测试环境、预发布环境通过环境变量控制切换。7.2 用Fixture组织测试数据我们在conftest.py中定义API客户端fixtureimport pytest from utils.api_client import ApiClient pytest.fixture(scopesession) def base_url(): return http://test-server:8000/api pytest.fixture(scopesession) def api_client(base_url): return ApiClient(base_url) pytest.fixture() def new_user_payload(): return {name: test_user, age: 18, email: testexample.com}注意scopesession的作用——同一个测试会话内该fixture只执行一次得到的客户端对象在所有用例中共享。创建连接是开销较大的操作会话级显著减少重复创建。而new_user_payload这种数据fixture不需要共享用默认的函数级作用域即可确保每个用例拿到的是独立副本互不干扰。这个概念很重要我单独解释一下fixture的四个作用域function默认每个用例都用一遍、class每个类用一遍、module每个模块用一遍、session整个测试会话用一遍。选择时遵循一个原则共享越多的东西越往上提但要注意共享带来的状态污染风险。比如数据库连接可以session级但数据库里的测试数据一定不能session级共享否则用例之间会互相干扰。7.3 接口用例的编写与参数化有了fixture获取用户信息的用例就变得非常简洁import pytest def test_get_user_success(api_client): resp api_client.get_user(1) assert resp.status_code 200 assert resp.json()[id] 1 assert resp.json()[name] zhangsan def test_get_user_not_found(api_client): resp api_client.get_user(99999) assert resp.status_code 404 pytest.mark.parametrize(user_id, expected_code, [ (1, 200), (99999, 404), (-1, 400), (abc, 400), ]) def test_get_user_various(api_client, user_id, expected_code): resp api_client.get_user(user_id) assert resp.status_code expected_code从代码可以看出test_get_user_various用一组参数覆盖了多种输入场景。如果后续需求增加了新的边界条件只需要在参数列表里加一行完全不用动代码逻辑。这种模式在接口测试中几乎是万能模板无论你是测登录、测下单、测查询只要把接口调用参数和预期结果提取成参数列表可维护性直接上一个台阶。创建用户的用例稍微复杂一点因为要先准备数据、调用接口、再校验结果def test_create_user_success(api_client, new_user_payload): resp api_client.create_user(new_user_payload) assert resp.status_code 201 user_id resp.json()[id] # 校验数据已真正写入 get_resp api_client.get_user(user_id) assert get_resp.status_code 200 assert get_resp.json()[email] new_user_payload[email]7.4 生成测试报告与覆盖率用例写完后运行并生成报告pytest testcases/ -v --htmlreport.html --self-contained-html --covutils --cov-reporthtml--covutils表示统计utils包的覆盖率--cov-reporthtml生成HTML格式覆盖率报告。如果你的CI流程需要覆盖率门槛可以在pytest.ini中配置[pytest] addopts --covutils --cov-fail-under80这样覆盖率低于80%测试流程就会直接失败强制团队保证测试质量。我刚给团队引入这个指标时大家怨声载道但跑了两个迭代后接口质量确实肉眼可见地提升了。覆盖率不是万能的但没有覆盖率底线测试很容易流于形式。8. 常见问题排查实录与避坑指南8.1 fixture作用域理解错误引发的诡异问题我接手过一个项目测试偶发失败排查了很久发现是某个fixture被设置成了scopesession其中保存了一份当前用户ID的变量多个用例对它进行读写产生了隐性的跨用例状态依赖。前面用例写进去的值改变了后面用例的执行结果。这种问题最坑的地方在于单独跑某个用例是好的按顺序全量跑也是好的但只要调整用例顺序或者跑特定的子集问题就出现。解决思路是fixture返回的数据如果会被修改尽量使用函数级作用域如果确实需要会话级共享也要确保共享的对象是不可变的或者每次使用前做深拷贝copy.deepcopy。这里的核心原则是fixture作用域越小状态污染的风险越低作用域越大性能收益越高——两者取平衡而不是一味求性能。我在项目里基本只有连接客户端这种无状态或低状态的对象才用session级。8.2 用例命名不符合规则导致静默跳过pytest的用例发现规则是文件匹配test_*.py或*_test.py类名以Test开头且不包含__init__方法函数名以test开头。很多时候你辛辛苦苦写了用例运行完发现collected 0 items大概率就是这三个条件没满足。这里有一个容易忽略的细节如果你定义的类不继承任何东西且类里面的方法名叫test_xxxpytest会正常收集。但如果你在类里定义了setup_method这类unittest风格的方法名pytest也会支持不过为了统一风格我会直接删除全部改用fixture。还有一点测试类不能用__init__方法pytest不会收集任何定义了__init__的测试类。这个坑看着小但几乎每个新手都会踩一次。8.3 断言失败后的定位技巧--pdb和-trace用例失败不可怕可怕的是不知道怎么快速定位。我推荐的排查流程是先看pytest给出的断言对比信息一般问题就明确了如果信息不够用pytest 用例节点 --pdb在失败点自动进入调试器检查当时的变量值、调用栈。进入pdb后可以用p打印变量、用w查看调用栈、用u/d切换栈帧非常灵活。还有一个参数是--trace它会在每个用例开始执行时就进入调试器适合需要逐步跟踪执行过程的场景。另外我觉得--maxfail1也很实用默认pytest会跑完全部用例才汇总但如果用例很多、你只是想改一个bug让它在第一个失败用例处停下来可以节省时间pytest --maxfail18.4 不要在测试里依赖用例执行顺序pytest默认按照文件内的定义顺序执行用例但这是实现细节而不是语言保证。特别是当你用了xdist并行后用例执行顺序完全不可控。一个优良的测试体系必须有测试之间互不依赖的自觉——每个用例都应该能独立运行、独立校验、独立清理。我在代码评审中最常打回的问题之一就是某个用例依赖前面用例创建的测试数据。正确的做法是每个用例自己创建所需数据用完自己清理或者至少确保清理逻辑不会影响其他用例。这就回到了fixture的威力——把准备数据和清理数据都封装在fixture里每个用例独立调用天然就隔离了。另外如果你发现自己的用例非得按顺序跑才能通过不要急着给pytest加--strict或者依赖排序插件先回头审视用例设计是不是出了问题。这往往不是框架的问题而是测试设计的问题。记住一句话不稳定的测试比没有测试更可怕因为它会耗费团队大量的信任成本。8.5 慢用例的排查与优化有时候跑了很久最后发现大部分时间耗在几个巨慢的用例上。pytest的--durations10参数会显示执行时间最长的10个用例这是一个非常好用的性能诊断工具pytest --durations10拿这个数据优先优化那几个耗时大户。常见的手段是把一次性初始化提升到session级fixture、用缓存减少重复网络请求、把耗时的外部调用替换成mock。我见过最离谱的情况是一个登录用例因为每次都重新建立数据库连接耗时8秒全量测试跑了300个这种用例就是40分钟。后来把数据库连接提升到session级线上回归时间直接砍半。我个人在实际操作中的一个体会是pytest的价值并不仅仅在于写起来少几行代码而在于它把测试的思维方式理顺了。写测试不再是为了交差而是成了一种跟代码对话的方式——你通过断言告诉程序你应该是什么样程序用通过或失败告诉你你当前是什么样。当你习惯了这种对话方式你对代码的信心和对改动风险的掌控感都会完全不一样。如果有条件把这些小技巧先用在一个不那么紧急的项目上一两个迭代你会慢慢感受到优雅到底意味着什么。