
你多久没有真正享受过写测试的过程了这是我从前同事那里听来的一句话当时我正对着一个接口测试的第N遍setUp方法发愁数据库连接、缓存清理、测试数据初始化全堆在一起还得小心tearDown别把别的用例依赖的数据删掉。用unittest写多了之后测试这件事就变成了“伺候框架”而不是验证逻辑正确性。后来我把一个Python项目整体迁移到Pytest情况彻底变了。这篇博文我来分享Pytest从0到1的完整落地路径为什么Pytest会让你觉得测试更优雅、环境怎么搭、断言怎么写、fixture机制是怎么回事、参数化和插件怎么让一份用例顶十份用以及我在真实项目里踩过的一堆坑。适合正在用unittest但总觉得别扭的人也适合刚接触Python自动化测试、想知道Pytest值不值得花时间学的新手。很多教程只讲API不讲设计理念看完还是不知道怎么写“优雅的测试”所以我尽量把验证过的好用写法直接摆出来配合代码一起看效果更好。1. 为什么说Pytest是“更优雅”的测试框架1.1 先看一段让我崩溃的unittest代码很多人的测试生涯是从unittest开始的我也不例外。早期我给一个计算器模块写测试大概是这个样子import unittest class TestCalculator(unittest.TestCase): def setUp(self): self.calc Calculator() def test_add(self): self.assertEqual(self.calc.add(1, 2), 3) self.assertTrue(self.calc.add(-1, 1) 0) def test_div(self): self.assertEqual(self.calc.div(6, 3), 2) with self.assertRaises(ZeroDivisionError): self.calc.div(1, 0) if __name__ __main__: unittest.main()这段代码本身不算复杂但仔细想想里面有不少“框架税”测试类必须继承unittest.TestCase不继承就识别不到有种为框架而框架的感觉。断言必须调用self.assertEqual、self.assertTrue、self.assertIn这些特定方法而不是直接用Python语言本身的判断表达式。每学一个新断言方法相当于给大脑多记一条API。初始化逻辑被固定成setUp和tearDown两个槽位所有测试方法共用。当两个用例需要完全不同的前置条件就得在setUp里写一堆开关和判断越写越乱。如果想要参数化unittest原生方案用subTest适合简单的循环但生成的用例名称不好看失败定位也比不上一行行的独立用例。当项目规模从五个用例膨胀到五百个用例时这些别扭会被放大setUp里的初始化逻辑越来越长测试之间的隐式耦合越来越多跑一次测试像在拆盲盒。这就是我开始寻找替代方案的直接原因。1.2 同样的逻辑Pytest只需要几行换到Pytest之后上面的测试变成了这样def test_calculator_add(): assert Calculator().add(1, 2) 3 assert Calculator().add(-1, 1) 0 def test_calculator_div(): assert Calculator().div(6, 3) 2 with pytest.raises(ZeroDivisionError): Calculator().div(1, 0)不需要写类不需要继承不需要self到处穿梭测试就是一个普普通通的函数。断言直接用Python原生assert关键字失败信息反而更清楚。异常检查用pytest.raises上下文管理器比assertRaises更贴近“with”的现代Python写法。我当时的直观感受是写测试终于回到了写普通函数的状态。我不用先去记“框架希望我怎么写”而是先想清楚“这个功能应该满足什么行为然后直接用语言表达出来”。这种从“面向框架写测试”到“面向逻辑写测试”的转变就是“优雅”的第一层含义。1.3 优雅不是玄学Pytest的核心设计哲学Pytest的优雅来自几个互相配合的设计选择第一测试就是一个普通函数。任何以test_开头的函数都会自动被收集不需要继承任何基类。普通函数能做到的拆解、复用、组合测试代码同样能做到。这一点被很多人忽略但它恰恰是Pytest能保持简洁的根本原因。第二断言就是assert。Pytest在导入测试模块时会重写断言失败时自动展开表达式中的左右值告诉你具体是哪一步推断出了问题而不是一句干巴巴的AssertionError。第三fixture采用依赖注入。测试函数需要什么数据就在参数里声明什么框架负责准备和清理。这比在基类里固定setUp/tearDown要灵活得多也是Pytest最值得深入学习的机制。第四约定大于配置。只要遵守命名规则零配置就能跑需要定制时可以一层层加配置犯不着为了跑通一个用例先写一堆样板。我把unittest和Pytest的几个体验差异整理成表格方便对照维度unittestPytest测试定义继承TestCase的类 test_方法普通函数或Test类test_开头即可断言self.assertEqual系列API原生assert失败信息自动内省前置/清理setUp/tearDown固定槽位fixture依赖注入按需声明参数化subTest相对笨重pytest.mark.parametrize清晰好用插件较少生态非常丰富用例收集命令行工具较薄弱-k、marker、--lf等筛选灵活表格只能概括框架差异真正体感上的区别要上手跑一遍才知道。下面就从环境搭建开始一步步来。2. 从零到第一个用例安装、目录约定与三步跑通2.1 安装与虚拟环境准备Pytest的安装非常轻量核心包基本没有重型依赖。但我还是建议先建虚拟环境再装尤其是同时维护多个Python项目的场景可以避免把不同项目的依赖混在一起。python -m venv .venv source .venv/bin/activate # Windows下执行.venv\Scripts\activate激活虚拟环境后安装pip install pytest pytest --version如果显示类似pytest 8.x.x的版本信息说明装好了。我这里有一个小建议也可以顺手安装pytest-cov和pytest-html覆盖率统计和HTML报告这两件事几乎每个项目都用得上后文会细说。2.2 目录与命名约定不写配置文件也能被“发现”Pytest最有魔力的地方之一是“说人话的约定”。它识别测试用例靠的是一套默认规则默认从当前目录递归查找文件名匹配test_*.py或*_test.py的文件。文件内的测试用例函数名必须以test_开头如果使用类类名必须以Test开头类内方法也以test_开头。被识别为测试的模块会被自动导入所以测试文件所在目录最好是一个有__init__.py的包避免同名模块冲突。一个最基础的目录结构长这样mydemo/ ├── calculator.py └── tests/ ├── __init__.py └── test_calculator.py有些初学者喜欢把测试文件和业务代码放在同一个文件夹前几个用例能跑通但项目一大就会乱。我现在的习惯是业务代码放顶层包测试集中放在tests目录必要时候再细分成unit和integration子目录。2.3 三步跑通第一个用例先写一个最简单的业务模块calculator.pydef add(a, b): return a b def div(a, b): if b 0: raise ZeroDivisionError(division by zero) return a / b再写测试文件tests/test_calculator.pyfrom calculator import add, div def test_add(): assert add(1, 2) 3 def test_div(): assert div(6, 3) 2然后在项目根目录运行pytest tests/ -q输出会显示类似2 passed的结果。如果想看每个用例的名字用-vpytest tests/test_calculator.py -vPytest的进度条输出非常直观实心点表示通过的用例F表示失败E表示用例执行过程中抛出了异常。刚上手时要养成看输出的习惯很多问题光看结果摘要根本定位不了。三个步骤就跑通了是不是比想象中简单不过别急着加点复杂操作先做一套最小配置把测试目录固定下来后面所有命令都会受益。在项目根目录新建pytest.ini[pytest] testpaths tests python_files test_*.py *_test.py addopts -vtestpaths限定了收集目录addopts是每次运行默认追加的参数。设好之后直接在根目录敲pytest它就知道该去哪里找用例输出也始终带详细用例名。对于刚入门的人来说这套配置相当于给Pytest画了一个小小的“家”省心很多。3. 断言之美写好assert失败信息自己会说话3.1 从assertEqual到assert少写一半样板代码断言是测试的灵魂。unittest把断言设计成一系列方法学起来像背APIPytest直接回归Python语言的assert这其实是一个更深层的选择assert是语言的一部分Python程序员每天都在用不需要额外记忆。我把常见的unittest断言和Pytest写法对照一下场景unittest写法Pytest写法相等self.assertEqual(a, b)assert a b不相等self.assertNotEqual(a, b)assert a ! b为真self.assertTrue(x)assert x为假self.assertFalse(x)assert not x为Noneself.assertIsNone(x)assert x is None包含self.assertIn(x, lst)assert x in lst抛出异常self.assertRaises(Exc, fn)with pytest.raises(Exc): fn()浮点近似self.assertAlmostEqual(a, b, places7)assert a pytest.approx(b)看到没有几乎所有unittest断言都能翻译成一个更自然的assert表达式。少了一层方法调用测试代码读起来就像在描述业务规则而不是在调用测试框架的命令。3.2 失败时为什么一眼能看到问题assert本身的行为是条件为假就抛出AssertionError。如果Pytest只做到这一步那它和普通Python脚本没什么区别。真正厉害的是断言内省机制——Pytest在导入测试模块的时候会重写assert语句失败时把表达式里的左右值都展开给你看。打个比方你写了一个错误的断言def test_add(): assert 1 1 3跑起来后Pytest的输出不只是告诉你“断言失败”而是给出类似这样的信息E AssertionError: assert (1 1) 3实际的项目里左右值往往是很长的字典、列表或对象Pytest会进一步展开内容显示两个对象在哪里不一致。这就不需要你再去加一堆print调试了。失败输出直接成为第一手排查线索。有个使用细节值得提一下如果assert条件后面跟了逗号和一个字符串这个字符串会作为失败时的自定义消息显示。例如def test_user_name(): user get_user() assert user.name tom, f用户名不是tom而是{user.name!r}这个技巧在项目后期特别有用。当别人看到一条失败用例时自定义消息能第一时间告诉他“这个断言是为了验证什么、预期是什么”而不是让人去猜。3.3 断言技巧浮点数、复合结构、异常与等待轮询写测试时最容易踩的坑是浮点数比较。1.1 2.2在计算机里并不精确等于3.3直接assert会失败。Pytest提供了pytest.approxassert 0.1 0.2 pytest.approx(0.3)approx还支持设置相对误差或绝对误差assert 123.456 pytest.approx(123.457, rel1e-3)复合结构断言也更省事。想验证字典里某几个键存在用子集比较assert {status: ok}.items() response.items()如果你担心items()在字典很大时性能不明显可以只比较关心的键assert response[status] ok assert response[code] 200异常断言除了检查“抛没抛”还想看异常内容可以把异常对象拿到手with pytest.raises(ValueError) as exc_info: parse_input(abc) assert invalid in str(exc_info.value)另外测试异步或外部服务时经常要“等一个状态出现”我习惯写一个简单的轮询工具import time def wait_until(predicate, timeout5, interval0.1): deadline time.time() timeout while time.time() deadline: if predicate(): return True time.sleep(interval) return False def test_event_processed(): assert wait_until(lambda: get_status() done)这类工具函数可以放在conftest.py里后续所有测试模块都能用。断言部分我会强调一点断言本身就是测试用例的文档写得清楚、有上下文提示维护成本会成倍下降。4. fixture机制用依赖注入替代setUp/tearDown4.1 从一个“每次都要重建连接”的例子说起unittest里想给多个用例准备数据库连接通常是在setUp里统一建class TestDB(unittest.TestCase): def setUp(self): self.conn create_connection(test.db) self.conn.clean()这个方法有两个体验问题一是所有用例必须用同一个setUp有些用例不需要数据库却也要承担初始化开销二是清理逻辑被塞进tearDown用例一旦抛异常清理是否执行、何时执行都不够直观。Pytest的fixture机制完全不同。先定义一个fixturepytest.fixture def db_connection(): conn create_connection(test.db) yield conn conn.close()然后测试函数通过参数名声明需要它def test_user_count(db_connection): assert db_connection.count_users() 0 def test_create_user(db_connection): db_connection.create_user(tom) assert db_connection.find_user(tom) is not None就是这么直接。测试函数觉得需要数据库就写参数不需要就不写框架照样跑。这种按需声明的方式让前置条件的依赖关系一眼可见。在unittest时代你得翻到setUp里才能看到这个类到底准备了多少资源在Pytest里参数列表就是依赖清单。4.2 yield一个fixture搞定准备和清理上面的db_connection用了yield这是Pytest 2.10之后推荐的写法yield之前的代码负责准备资源yield之后的代码负责清理资源无论测试用例是成功还是抛异常清理代码都会执行。这一点在实际项目中非常关键比unittest的tearDown可靠得多因为tearDown虽然也会在失败时执行但它的时机和作用域是固定的不如fixture里这种“紧挨着资源定义”的写法清晰。举个临时文件的例子pytest.fixture def temp_file(tmp_path): p tmp_path / data.txt p.write_text(hello) yield p # 临时目录由Pytest自动清理这里不需要额外代码 # 但可以在yield后面做一些显式清理Pytest自带的tmp_pathfixture就是典型的依赖注入资源每个测试用例都能拿到一个专属的临时目录互相之间不会干扰。在unittest里想模拟这种能力要么自己管理临时目录要么依赖第三方库。Pytest直接解决了。4.3 作用域、自动使用与conftest共享fixture默认是函数级别的每个测试用例运行前都会执行一次准备和清理。这在很多场景是合理的但有些资源比较重比如数据库连接、读取一次配置文件没必要每个用例都创建销毁。fixture支持scope参数作用域说明适用场景function每个用例执行前后各一次默认适合轻量数据class每个测试类执行一次类内用例共享资源module每个测试模块执行一次加载一次配置、数据文件package每个测试包执行一次包级公共资源session整个测试会话执行一次数据库连接、HTTP服务实例用法很简单pytest.fixture(scopesession) def db_conn(): conn create_connection(test.db) yield conn conn.close()需要注意session作用域的fixture在整个测试期间只有一个实例如果某个用例修改了它的状态后面的用例会接受到“被污染”的数据。我后文会专门讲这个坑。有时候我们希望某些用例每次都自动使用某个fixture不想逐个声明参数。fixture支持autouseTruepytest.fixture(autouseTrue) def clean_cache(): cache.clear() yield cache.clear()但我的经验是autouse要克制尤其在全局conftest.py里。一上来全自动会让fixture之间的依赖关系变得隐晦反而失去“参数列表即依赖清单”的可读性。最好把autouse限制在确实“每个用例都离不开”的资源上比如环境清理。conftest.py是Pytest的共享机制核心放在某个目录下的conftest.py里定义的fixture对该目录及子目录的所有测试可见。项目根目录的conftest.py相当于全局资源库这是组织公共fixture最自然的做法。4.4 我踩过的fixture坑fixture虽好我也没少吃它的亏挑几个典型的说说。第一个是参数名拼写错误。fixture通过参数名匹配比如测试函数把db_connection写成db_connectionnPytest会直接报错说缺失fixture同时附上可用fixture列表。报错本身很清晰但如果你在一个大工程里到处拷贝代码这种低级错误还是会耽误几分钟。我的办法是开编辑器自动补全或者复制fixture定义处的精确名称。第二个坑是session作用域的可变状态污染。我曾有一个session作用域的fixture返回一个缓存字典某个测试用例往里面塞了数据后面所有用例都拿这个被污染的数据。修复方法有两个如果资源的创建成本不高就把作用域改成function如果必须跨用例复用就让fixture返回只读对象或副本。第三个坑是autouse范围铺太宽。为了图省事我在根conftest.py里给很多fixture加了autouseTrue结果每个用例都在重复执行不必要的准备逻辑一套测试跑下来慢了两分钟。后来我只保留了必需的自动清理fixture其余改回显式声明速度立刻恢复正常。第四个坑和yield有关。如果fixture里yield之前的代码抛了异常那yield之后的内容根本不会执行。大多数情况下这是合理的比如数据库连不上清理代码没有意义但如果你把“无论准备成功与否都要做的复位动作”放在yield后面它就不会被执行。正确做法是把复位动作放到finally里或者放在yield之前用try/finally层层嵌套。我在早期写fixture时确实在这里栽过跟头。5. 参数化与插件生态用一份用例覆盖一片需求5.1 参数化一份逻辑多种输入真实项目的测试往往不是“一组输入验证一个行为”而是“同一套逻辑要覆盖很多边界情况”。如果每个用例都复制一遍函数测试代码会膨胀得很厉害。Pytest的pytest.mark.parametrize直接解决这个问题。比如要验证字符串长度函数import pytest pytest.mark.parametrize(input_str,expected, [ (abc, 3), (, 0), (hello world, 11), (你好, 2), ]) def test_string_length(input_str, expected): assert my_length(input_str) expected运行时会生成四条独立的用例每条都有自己的名字失败时能精确定位是哪一组参数出问题。这个能力看着简单实际价值非常大我接手过的老项目里手工复制粘贴几十个相似用例的情况比比皆是后来用参数化重构删掉了一半代码测试覆盖范围还更全了。参数化还支持叠加形成多组参数的组合pytest.mark.parametrize(a, [1, 2]) pytest.mark.parametrize(b, [3, 4]) def test_combo(a, b): assert a b 0这里会执行13、14、23、24四种组合适合笛卡尔积类场景。如果组合过多可以只传元组列表用单层参数化控制。为了让参数化用例的名字更可读可以用ids参数pytest.mark.parametrize(input_str,expected, [ (abc, 3), (, 0), ], ids[basic, empty]) def test_string_length(input_str, expected): ...不传ids时Pytest也能生成名字但长字符串参数会让用例名变得很长影响查看报告。5.2 插件生态别自己造轮子Pytest的强大一部分来自插件生态。我常用的几个插件每一个都解决一类真实痛点插件作用典型命令pytest-cov统计代码覆盖率pytest --covsrc --cov-reporthtml tests/pytest-html生成可视化HTML测试报告pytest --htmlreport.htmlpytest-xdist并行执行测试加速pytest -n auto tests/pytest-timeout给用例设置超时时间pytest --timeout30 tests/pytest-rerunfailures失败用例自动重跑pytest --reruns 2 tests/pytest-ordering控制用例执行顺序pytest.mark.run(order1)安装非常简单pip install pytest-cov pytest-html pytest-xdist pytest-timeout pytest-rerunfailures pytest-ordering使用上我有一点心得pytest-xdist加速明显但并行执行时多个进程会同时操作公共资源如果测试里有共享文件、共享数据库容易出现偶发失败。真要用并行先把测试设计成互相隔离或者只对不依赖外部状态的纯逻辑测试开并行。pytest-rerunfailures适合处理一些外部服务不稳定导致的偶发失败但它可能掩盖真正的bug我建议只对明确标记为“网络相关”或“第三方集成”的用例开启重跑而不是全局重跑。覆盖率统计是一个容易被低估的好工具。pytest --covsrc --cov-reporthtml tests/跑完会生成一个htmlcov目录浏览器打开就能看到每个模块的哪些行被测试执行过。我维护老项目时先用覆盖率找“从未被执行过”的分支优先给这些模块补测试比盲写用例高效得多。5.3 标记与选择性执行测试套件做减法当测试用例多起来你不可能每次都全量跑。Pytest提供了一套精确的筛选手段。自定义标记最常见的场景是把用例按“执行速度”或“业务模块”分类pytest.mark.slow def test_heavy_computation(): ...运行pytest tests/ -m not slow pytest tests/ -m slow用了自定义标记记得在pytest.ini里注册一下不然会有warning提醒[pytest] markers slow: marks tests as slow integration: marks tests as integration-k表达式可以直接按用例名过滤处理临时需求很好用pytest tests/ -k add or div pytest tests/ -k not network跳过用例和预期失败也有两个常用装饰器pytest.mark.skip(reason接口尚未实现) def test_new_api(): ... pytest.mark.xfail(reason已知问题暂未修复) def test_known_bug(): ...前者的含义是“这个用例现在不该跑”后者的含义是“这个用例现在会失败但失败是预期中的”。用xfail管理未知bug列表可以让测试套件保持绿色同时不遗忘问题。还有一个我每天都用的筛选命令pytest tests/ --lf--lf表示“只跑上次失败的用例”非常适合开发过程中连续迭代。配合--ff可以在全量跑时把失败用例放在最前面第一时间发现问题。6. 真实项目落地目录组织、常见坑与CI接入6.1 测试不是越多越好先写核心路径聊完基础语法讲讲我落地项目时的策略。很多新手进入Pytest后容易走向另一个极端看到什么都想写测试把私有方法、临时脚本、甚至一行getter都盖满用例。我的建议是优先写“有价值、易回归、影响关键业务”的用例。我通常按这个优先级排纯函数和工具模块输入输出稳定测试成本低收益率高。数据模型和校验逻辑比如用户状态机、金额计算这类逻辑出bug影响大。对外接口的契约测试确保接口参数、返回结构、错误码符合文档约定。数据库操作和外部服务集成放在集成测试目录适当使用重跑和超时。测试设计上优先保证核心路径再慢慢补充边界和异常分支。覆盖率要达到100%往往性价比很低大部分项目保持在核心模块80%以上整体70%上下就已经能发挥很好的守护作用。6.2 一个完整项目的目录参考我维护的一个Python后端服务测试目录长这样backend/ ├── app/ │ ├── services/ │ ├── models/ │ └── main.py ├── tests/ │ ├── conftest.py │ ├── unit/ │ │ ├── test_service_a.py │ │ └── test_model.py │ ├── integration/ │ │ └── test_api.py │ ├── __init__.py ├── pytest.ini ├── requirements-dev.txt └── .gitignoreconftest.py放在tests根目录这样它里面的fixture对所有子目录可见。典型的conftest.py内容包括临时数据库连接、测试客户端对象、公共参数化数据、autouse的环境清理。我习惯把单元测试和集成测试分开目录。原因很简单单元测试希望跑得快、没有外部依赖集成测试可能要连数据库或启动测试服务跑得慢。分开之后开发时可以只跑pytest tests/unit获得快速反馈提交前再全量跑一次。6.3 常见坑与解决方式我把自己在真实项目中遇到的Pytest问题整理成清单都是踩过之后才明白的。第一个是中文输出乱码。Windows下跑Pytest用例名称或断言信息里带中文时控制台偶尔显示乱码。我一般用PYTHONIOENCODINGutf-8环境变量或者在pytest.ini里配置[pytest] addopts -p no:cacheprovider --coloryes不过--coloryes和编码无关只是让输出更清楚。严格来说最彻底的办法是让用例名称保持英文中文只出现在自定义断言消息里因为pytest-html生成的报告对中文ID支持也不算好。第二个是测试之间的隐性耦合。我见过一个项目测试A创建了用户测试B假设这个用户已经存在遇到全量跑能过、单独跑必挂的情况。这类问题根源是测试执行顺序被当成了依赖条件。Pytest默认按文件顺序执行但-n auto并行时顺序完全不可控。解决方案是每个用例独立准备自己需要的数据用fixture返回专属临时状态不要依赖其他用例产生的副作用。如果你必须依赖全局状态把这个状态做成session或module作用域的fixture并明确文档化。第三个坑是tmp_path虽然好用但tmp_path_factory在session作用域的fixture里不能直接当参数用。想做一个“整个测试会话只创建一次的临时文件包”可以这样pytest.fixture(scopesession) def session_tmp(tmp_path_factory): return tmp_path_factory.mktemp(data)记住普通tmp_path只在函数或类作用域可用跨会话复用必须用tmp_path_factory。第四个是参数化用例太多导致报告冗长。一次给50组参数跑出的结果非常壮观但真正失败时定位并不轻松。我习惯给每组参数写有意义的ids并想清楚这条用例“失败到底代表什么业务问题”而不是简单地把参数堆上去。第五个容易忽视的坑是.pytest_cache和__pycache__目录被误提交到代码库。Pytest运行时会生成.pytest_cachePython运行时会生成__pycache__这些应该在.gitignore里排除.pytest_cache/ __pycache__/ *.pyc htmlcov/如果忘了加迟早会在代码评审时被同事点名。这类“非功能性问题”不会让测试失败但会让仓库变得很乱。6.4 把Pytest接到CI流水线本地能跑通只是第一步真正的价值在于每个人提交代码后都能自动验证。我用的CI方案就放在仓库根目录的GitHub Actions配置里name: test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - run: pip install -r requirements-dev.txt - run: pytest tests/ --covapp --cov-fail-under80这里的--cov-fail-under80会在覆盖率低于80%时让CI失败等于用硬指标提醒团队不要透支测试覆盖率。实际上覆盖率阈值要根据项目情况定新老项目标准可以不一样但总好过完全没人关注覆盖率。CI里还有两个细节值得注意第一如果用例里有需要外部服务数据库、缓存的集成测试最好在CI配置文件里单独启用服务容器而不是靠Mock糊弄过去第二CI的Python版本和本地要尽量一致我在本地用3.12、CI用3.8导致过好几个低级报错。最后说说我自己实际使用的一点体会。从unittest迁到Pytest之后我写测试的心态变化是实实在在的以前是“为了完成指标去填模板”现在是“先把业务逻辑的逻辑讲清楚再配合fixture和参数化把边界堵住”。如果你正被unittest的样板代码折腾得够呛可以试试从最小的模块开始先用Pytest写三五个用例跑通后再逐步引入fixture、参数化、覆盖率、并行执行一次加一样就好。比硬啃一套大而全的测试方法论要有效得多。希望这篇文章能帮你少走一点弯路早一点体会到“更优雅的测试”到底是种什么感觉。