你还在用unittest的assertEqual吗你还在手动编写setUp和tearDown或者为每一个测试准备临时数据库吗如果答案是肯定的请合上本指南因为你还没准备好拥抱pytest。pytest不只是一个测试框架它是一套让测试代码成为项目资产的设计哲学。测试本身不是待办事项而是设计说明书。当你写完一段产品代码第一个想知道的就是它的行为是否正确pytest正是为了把这件最自然的事做到极致而存在。安装pytest只需一行命令pip install pytest。然后你编写一个以test_开头的函数使用普通的assert再用pytest运行。就这么简单。不需要继承任何基类不需要使用任何特殊内置函数。不需要类继承不需要固定命名pytest用最小约定解放你的创造力。这种极简主义或许会让人怀疑这么简单的框架能处理复杂的测试场景吗答案在接下来的内容中。从unittest到pytest比你想的更远unittest脱胎于Java的JUnit深得“框架”真传你必须创建TestCase的子类必须定义setUp和tearDown必须使用assertEqual这个命令式方法。一系列强加的结构不但没有帮助反而把测试变成了机械的重复劳动。pytest颠覆了这一切。它允许你写自由的函数甚至可以把测试函数定义在模块级。测试不是把代码塞进框架的模板而是用函数描述行为。pytest的断言就是Python原生的assert语句。你不需要背assertTrue、assertFalse、assertListEqual的拼写。如果你想检查一个列表是否为空直接assert not items。如果你的对象有自定义的__eq__那么assert obj1 obj2完全可行。断言的重写机制是pytest称霸测试界的秘密武器当断言失败时pytest会捕捉你的原表达式输出实际值和预期值及其上下文。比如assert user.age 30失败你会看到类似assert 25 30的清晰信息。断言越具体失败信息越有价值。安装与基本运行在激活虚拟环境后安装pytest然后在一个目录下创建test_sample.pydef test_addition(): assert 1 1 2在命令行执行pytestpytest会自动递归查找当前目录下所有test_.py文件和_test.py文件收集以test_开头的函数或类中的测试方法。默认情况下每个测试通过只输出一个点失败输出F错误输出E。运行结束后会有一个汇总。这种安静模式的优雅在于只有当问题出现时它才打破沉默。命令行选项是你更快工作流的杠杆。-v让你看到每个测试的名字和结果-q则只输出最终统计。-s允许打印输出--pdb在失败后进入调试器-k按名字表达式过滤-m按标记过滤。掌握这些你就能像外科医生一样精准地运行测试。测试不是一场赌博你要知道哪些该跑、哪些该跳过。fixture依赖注入的优雅实现fixture是pytest最精彩的设计。它允许你定义一些前置的资源和生成逻辑并通过参数名自动注入到测试函数中。与其在每个测试里手工创建连接、读取配置或清理残留不如把这些职责封装在一个fixture里。看一个简单例子import pytest pytest.fixture def user(): return {name: Alice, age: 30} def test_user_age(user): assert user[age] 30当pytest发现测试函数有一个名为user的参数时它会自动寻找同名fixture调用它把返回值传递给测试。fixture还支持依赖注入——一个fixture可以依赖另一个fixturepytest.fixture def db(): return connect() pytest.fixture def repository(db): return Repository(db) def test_repository(repository): assert repository.is_ready()这种方式让依赖关系变得清晰透明。fixture是pytest的灵魂没有fixture的pytest只是一个漂亮的花瓶。不要止步于此fixture还有更多威力。fixture的作用域决定它被调用的频率。默认是function每个测试调用一次class作用域每个测试类调用一次module作用域每个模块调用一次session作用域整个测试会话只调用一次。如果某个fixture的创建开销很大比如启动浏览器、加载大型模型、连接远程数据库你可以把它设置为scopesession。正确选择fixture作用域是优化执行时间的头号杠杆。但要警惕session级fixture在修改状态后可能污染其他测试最好让这类fixture只提供只读的共享资源。fixture也可以做清理只需在fixture中使用yield代替returnpytest.fixture def temp_file(): f open(/tmp/testfile, w) yield f f.close()这样无论测试是否抛出异常yield后面的代码都会执行甚至还能处理reset操作。这比unittest的tearDown更优雅因为初始化与清理代码紧挨在一起。从视觉上就能看出哪个步骤对应哪个动作无需跳转方法名。conftest.py共享夹具的秘密武器通常你会把fixture写在conftest.py中pytest会自动从目录向上查找。conftest.py中的fixture可以被同目录以及所有子目录的测试共享无需import。conftest.py不是杂货铺别把所有fixture都堆进去。那些只被一个模块使用的fixture应该直接放在那个模块里只有跨模块共享的才提升到conftest.py。否则你的代码会变得难以追踪。conftest.py还可以定义钩子函数、命令行选项和初始化的前置操作。比如你可以在这里添加一个自定义的--fast选项用于跳过慢测试。或者定义pytest_collection_modifyitems钩子对收集到的测试项进行排序或标记。钩子机制让pytest成为可以自由定制的测试操作平台。参数化测试一页用例千行数据测试同一功能的多个输入输出是测试最常见的重复来源。unittest曾要求你使用子测试或者编写循环但pytest提供了更优雅的装饰器。使用pytest.mark.parametrize可以传入一组数据每个数据项独立成为一个测试用例并显示对应的参数值import pytest pytest.mark.parametrize(celsius, fahrenheit, [ (0, 32), (100, 212), (-40, -40)]) def test_temperature(celsius, fahrenheit): assert celsius 9 / 5 32 fahrenheit运行它你会看到三个独立的测试名如test_temperature[0-32]。如果其中一项失败其余成功你能精确定位是哪一组输入引发的问题。参数化不只是减少重复更是把测试数据从逻辑中抽离出来形成可维护的数据表。当测试数据与测试逻辑彻底分离你得到的不仅是减少的代码行数还有清晰的数据来源。还可以组合多个parametrize装饰器生成笛卡尔积。比如测试一个HTTP客户端你可以传入不同的URL、方法、headers和body。更高级的参数化可以通过pytest_generate_tests钩子动态生成但那需要更深的定制。数据驱动测试是pytest最具生产力的特性之一。断言的艺术让失败明明白白pytest对普通assert进行了重写。这种重写发生在字节码层面当断言失败它会把复杂的表达式拆解并给出详细的差分。你可以这样写assert result.status_code 200 and success in result.text失败时pytest显示两个子表达式的实际值。断言不是检验而是编程语言的一部分。你不需要学习一套新的断言方法。浮点数比较是另一个坎。直接比较0.1 0.2 0.3会失败因为二进制浮点误差。pytest提供了pytest.approxdef test_float(): assert (0.1 0.2) pytest.approx(0.3)approx还可以指定相对误差和绝对误差。不要被浮点数的魔法迷惑用approx来尊重数据的客观精度。检查异常用pytest.raises。测试中不应捕获异常而是让异常成为测试用例的一部分。你可以检查异常类型和消息import pytest def test_error(): with pytest.raises(ValueError, matchinvalid value): parse_input(abc)match参数使用正则表达式匹配异常消息比只检查类型更加严格。还可以使用as关键字获取异常实例做更多验证。这些细节让你的测试文档化。还有pytest.mark.skip和pytest.mark.xfail。前者用于已知不通过但尚未处理的用例后者用于预期失败的用例。预期失败不是失败而是对需求的明确规定。使用xfail时如果测试意外通过pytest会报告“XPASS”提醒你更新状态。这比注释掉测试要安全得多。标记与选择如何只跑需要的测试在大型项目中你可能需要把测试分组快速回归、慢速集成、用户界面等。pytest标记可以让你为测试添加任意标签。pytest.mark.slow、pytest.mark.ui然后在命令行使用-m slow或-m not slow来选择。给测试打上标签相当于为你的质量保障体系绘制了地图。标签还能组合比如-m not slow and not ui。如果你只想运行名字包含特定字符串的测试-k选项支持表达式匹配如-k test_user or test_order。结合--lflast failed可以只重跑上次失败的测试。关注失败比全量重跑更聪明。在开发过程中先用-k过滤出相关模块提交前再跑全量这能节省大量时间。插件生态pytest的无限魔力pytest本身很轻但插件生态让它所向披靡。安装pytest-cov你可以获得覆盖率统计。覆盖率不是万能的但没有覆盖率测试的测试如同盲人摸象。pytest-xdist让你的测试并行执行。只需在命令行添加-n autopytest会自动启动多个worker进程。并行化的前提是测试之间互不依赖这强烈鼓励你写干净的测试。如果你的测试不能并行那不是pytest的问题而是你的测试互相污染了。pytest-mock提供了mockerfixture让你更简洁地替换外部依赖。pytest-django为Django项目提供数据库和客户端支持。pytest-timeout为测试设置超时防止程序卡死。pytest-benchmark可以量化性能。每个插件都像是一个新模块它们组合在一起让pytest能够应对几乎所有的测试顽疾。组织测试结构的智慧测试文件的组织方式直接反映项目的可维护性。常见的最佳实践是建立tests/目录在内部按照unit/、integration/等子目录划分或者直接按被测模块组织。测试目录的结构就是项目架构的镜像。每个测试模块的命名应明确对应被测模块例如test_author.py对应author.py。这样当测试失败时你能快速定位是哪个业务逻辑出了问题。conftest.py可以放置在任何目录层级。你可以在tests/下定义一个目录级的conftest提供针对“数据库连接”的高层fixture然后在tests/unit/下定义更底层的conftest提供“内存中的测试数据”。这种嵌套关系让fixture的作用域清晰。记住fixture的存活范围与摆放位置强相关。还要注意测试的隔离性。不要依赖测试执行顺序不要共享可变对象而不加清理。每个测试都该是孤岛只能靠fixture获得依赖。这通常意味着你要避免使用全局变量并且在每个测试的fixture中创建全新的对象。pytest的默认作用域function已经在帮你实现这一点但如果你使用了session级fixture就要格外小心。性能与策略让测试“快”起来当测试套件膨胀到数千个用例运行时间成了项目管理的一部分。除了并行运行-n你还可以对fixture进行profile。pytest可以添加--durations10显示最慢的10个测试。这些数据往往揭示出意外的性能瓶颈——也许是一个测试重复初始化了昂贵的日志对象。别为了省几行代码而把整个测试套件拖成蜗牛。把慢测试标记为slow让它们在CI流水线中单独运行而日常开发只跑快速测试。另一种策略是通过缓存减少重复工作。pytest内置了--lf和--failed-first让失败的测试优先执行快速得到反馈。在快速反馈与全面验证之间找到平衡是工程效率的关键。有时候测试过于复杂是因为生产代码的依赖过多。尝试重构你的代码让它们更容易测试。困难往往不是测试的问题而是设计的不合理。依赖注入模式、使用抽象边界、避免单例这些在测试中会一一显现。避开陷阱常见错误及正确解你可能会遇到一些看似奇怪的测试失败。最常见的是“测试之间相互影响”。比如某个fixture修改了全局环境变量而另一个测试假定环境变量是默认值。解决方案是把环境变量也放入fixture并在teardown中恢复。测试必须能够任意独立运行。如果你发现自己被顺序问题折磨请立刻检查是否使用了不可变的数据结构。过度mock是另一个普遍现象。当你模拟了所有外部服务测试就跑得飞快但一旦真实接口变了你的测试依然绿。mock应该隔离不确定性而不是掩盖真实性。对于第三方API你可以使用responses或requests-mock提供可预测的响应但最好只在集成测试的边界使用。单元测试里mock掉一个函数也许可以但不要mock掉一切。断言不够精确也会让测试失去意义。比如assert response对于0或都会通过而你可能想验证的是状态码。请使用显式断言assert response.status_code 200。不要把“真值”当作“正确”。还有不要在测试中打印大量的中间结果然后人眼确认。测试的唯一输出就是通过或失败。如果你需要调试先用pytest --pdb进入交互式调试器。如果测试逻辑本身太复杂那就把它拆成更小的步骤。深入钩子与自定义插件pytest的真正威力在于可扩展性。你可以通过钩子函数改变收集、执行、报告的任何阶段。例如定义一个pytest_runtest_setup钩子在每个测试之前做权限检查定义一个pytest_terminal_summary钩子在结束时输出额外的统计数据。自定义插件可以是简单的模块也可以打包发布到PyPI。写pytest插件是把你团队最佳实践固化为工具的最有效路径。一个经典的例子是实现“重试验证”。pytest的--reruns插件只能做固定次数的重试但你可以通过钩子实现更智能的重试策略。或者你可以实现一个“报告输出到JIRA”的插件让失败的断言自动创建工单。当你熟练掌握了钩子pytest就不再是测试工具而是测试基础设施。结语你大概已经意识到pytest的魅力不在于它能替你写测试而在于它让写测试的过程变得近乎愉悦。它用最少的规定换来了最大的自由度用fixture和参数化等机制把重复和混乱拒之门外。当你开始享受写测试的时候你的工程素养已经站在了新的高度。记住测试代码不是被消费的而是被维护的。它和产品代码一样需要设计、重构和纪律。不要用框架限制你的表达而是用约定解放你的思想。下次当你面对一个看似复杂的测试场景先想想pytest能给你什么帮助。答案可能会让你惊喜。