☰
Python单元测试入门到进阶:pytest、fixture、mock与覆盖率实战
2026/10/21 18:12:36 网站建设 项目流程

写下这个标题的时候,我想起很多刚写完业务代码的开发者对我说过的同一句话:“逻辑又不复杂,为什么要写测试?”这话放到两年前我也是同意的。直到有次改了一个看似无关紧要的配置项,结果把另一个模块的默认行为带偏,整整一个下午都在排查线上告警,我才真正理解单元测试对于“可维护”“可演进”的代码有多重要。它不只是一堆 assert,也不只是给领导看的覆盖率报告,而是一套能帮你兜底、帮后来者理解意图、逼着你写出低耦合代码的工程防线。这篇内容面向有 Python 基础、正在往进阶迈步的开发者,重点讲清楚单元测试的常见工具、写法取舍,以及我从实际项目中踩过的坑和小技巧,希望对想补上这一课的人有直接帮助。

1. 为什么进阶第一步要补单元测试这堂课

1.1 单元测试到底在测什么

单元测试这个词听起来高大上,本质上却非常朴素:它拿一个函数、一个方法或者一个类当成最小单位,喂给它一组输入,然后断言它返回的结果是否符合预期。就像你在流水线上给每个零件配了一个检验工位,零件合格才放过去,不合格则亮红灯。放在代码里,这个检验工位就是一个个测试函数,红灯就是一个失败的断言。

但进阶的视角会稍有不同。单元测试不只是验证“现在对不对”,而是在把一个行为的规则固定下来。比如一个日期格式化函数,它应该接受什么格式、非法输入怎么处理、边界条件是什么,这些一旦被测试用例描述清楚,就等于给未来的修改立了契约。之后任何人改动这个函数,只要跑一遍测试,就能立刻知道有没有违反契约。我见过不少开发者把单元测试当成额外负担,实际上它更像一套自动化的“行为需求文档”,而且这份文档不会说谎。它不会抱怨你文档写得太长,也不会因为情绪影响判断,只会在约定被破坏时精准地站出来。

1.2 它真正解决的四类问题

第一类是回归保护。这是单元测试最直接的收益:你在老系统里加了新功能,或者重构了一个内部函数,担心影响其他模块,只要测试覆盖到位,一条命令就能把隐患暴露出来。没有测试的时候,只能靠“人肉回归”,点遍所有入口,既慢又容易漏。很多项目后来不敢动旧代码,就是没有一个安全网,而单元测试就是那张网。

第二类是代码可读性。一个写好的测试用例,本身就是最好的调用示例:它展示了函数怎么构造参数、返回什么结构、异常怎么抛出。新成员接手项目时,与其翻一遍业务代码,不如先读测试,理解速度会快很多。你会发现,测试写得清楚的项目,团队成员之间的沟通成本也会低一些,因为很多“这个函数到底怎么用”的疑问,打开测试文件就有答案。

第三类是设计反馈。经常有人问我,为什么自己写的代码总是测不动?因为被测代码往往耦合了太多东西:全局变量、直接访问数据库、把网络请求写在业务逻辑里。当你想为它写测试时,那些别扭就会变成一道道障碍,逼着你去拆分职责、显式传参。这种“被测试逼出来的设计改进”,价值远超测试本身。换句话说,写测试的过程其实是一次免费的代码评审,它会逼你用更清晰的接口来表达意图。

第四类是调试成本控制。有了测试,定位 bug 的速度会快很多。你不需要通过日志一层层猜,只需要跑定向用例、看失败断言,很快就能确定是业务逻辑错了还是数据没对齐。尤其是那种“改一处崩一片”的老项目,一条失败的测试能直接告诉你崩在什么地方,而不是让你在前端、后端、数据库之间来回翻线。

这些内容听起来像理念,其实都体现在一行行代码里。接下来我先把工具选型讲清楚,再进入具体写法。

2. 工具选型:unittest 还是 pytest

2.1 unittest:经典但略显笨重

unittest 是 Python 标准库自带的测试框架,最大的优点就是不用安装第三方包,环境再干净也能直接跑。它的核心模型和 JUnit 一脉相承,用类和方法组织用例,支持 setUp、tearDown 这种前后置逻辑,还自带了一批断言方法,比如 assertEqual、assertTrue、assertRaises。对于只求快速验证的小脚本,unittest 完全可以胜任。

但它的别扭之处也很明显。以我自己的感受来说,写一组简单的数据校验测试,用 unittest 要定义一个 TestCase 子类、实现 test 开头的方法,还要注意继承关系;如果稍微涉及公共初始化,就得考虑 setUp 这些钩子。代码看起来会有不少模板感,可读性稍差。而且 unittest 的断言方式跟 Python 原生的 assert 不同,你得记它的方法名,有时候写起来总觉得绕。

当然,这不代表 unittest 就没有价值。很多历史项目、系统自带包、某些工具链仍然以 unittest 为基础。即便你打算全面采用新框架,也需要认得 unittest 的写法,因为接手老项目时大概率会撞上它。我有一个原则:标准库的能力可以不用,但不能不认得,尤其是在没有网络环境、装不了第三方包的部署环境里,unittest 是你最后的兜底方案。

2.2 pytest:为什么它成了事实标准

pytest 的核心优势是全方位的“轻”。它没有强制类结构,普通的函数就可以当测试用例,交给它时只要函数名以 test_ 开头,就能自动收集。断言直接用 Python 原生的 assert,失败时 pytest 会通过差异对比告诉你哪两个值不一致,信息更直观。刚上手的人不需要理解什么框架哲学,写一个函数、写一个 assert,就能跑起来。

真正让它拉开差距的是两个特性:fixture 和参数化。fixture 是一个灵活的依赖注入机制,可以非常优雅地处理测试前的数据准备和环境搭建;参数化则让你用极少的代码覆盖多组输入输出。再加上体量庞大的插件生态,比如测试报告、覆盖率、超时控制、重跑失败用例等,都是插拔即用。论上手难度,pytest 比 unittest 更低;论进阶能力,它也经得起重度使用。

对比项unittestpytest
用例组织类和方法普通函数即可
断言方式专用断言方法原生 assert,带差异提示
前置/清理setUp / tearDownfixture,支持依赖注入
参数化需要 subTest 或手工循环内置 parametrize
插件生态较少丰富
学习曲线中等平缓

这个表不是我硬凑的,而是实际写过两种框架后的直观感受。pytest 的插件机制还有一个好处:你不需要把各种能力内置到框架里,按需加插件就行。这让 pytest 本身保持轻量,也让团队可以按自己的工程规范组装测试工具链。

2.3 选型建议:别纠结,看场景

给一个简单可操作的结论:如果是新项目,尤其是你想把测试作为日常习惯来培养,直接用 pytest,能够降低写测试的心理门槛,生态也更好。如果是维护老项目,老项目用什么你就跟着用什么,没必要强行引入新框架。很多时候纯标准库环境不允许加包,unittest 也一样能完成工作。

有一点要注意:无论选哪个,测试框架只是载体,核心是测试思维。别陷入框架之争而忘了正事。下面我就以 pytest 为主线,把整个测试搭建过程从头走一遍。你会发现,很多思路在 unittest 里也能迁移,只是语法稍有不同。

3. 从第一个测试用例开始:断言是骨架

3.1 一个最小可跑的测试文件

先拿最常见的计算函数举例。假设我写了一个除法函数:

# core_math.py def divide(a: float, b: float) -> float: if b == 0: raise ValueError("除数不能为0") return a / b

同一目录下建一个 test_core_math.py:

import pytest from core_math import divide def test_divide_normal(): assert divide(10, 2) == 5 def test_divide_zero_raises(): with pytest.raises(ValueError): divide(10, 0)

然后在命令行里执行pytest -q,跑完会看到两个点,表示两个用例通过。如果你只有一个测试文件,也可以执行pytest test_core_math.py -q精确指定。这些基础命令很简单,但建议把它们固化在一个命令里:pytest -q --disable-warnings,因为项目大了之后,pytest 可能会输出版本不匹配的警告,默认不显示有时候反而更清爽。

需要特别提醒:测试文件名一定以 test_ 开头(或以下划线 _test.py 结尾),pytest 的默认收集规则只认这种模式。如果你起名不规范,即使写了用例也跑不到,这是新手最容易踩的第一个坑。另外,测试文件最好和被测试模块保持清晰的路径关系,比如 tests 目录放测试,src 目录放业务代码,再用 pytest.ini 或 pyproject.toml 指定 testpaths,以免收集范围过大。

3.2 断言方式与常见陷阱

断言是整个测试最重要的部分。pytest 里直接用 assert 表达式,失败时会显示比较结果。比如:

def test_dict_compare(): user = {"name": "A同学", "age": 20, "active": True} assert user["age"] > 18 assert user.get("email") is None

浮点数比较是高频坑。由于二进制浮点误差,0.1 + 0.2 == 0.3在多数语言里都是 False,Python 也不例外。正确做法是用pytest.approx:

def test_float(): assert (0.1 + 0.2) == pytest.approx(0.3)

异常测试用pytest.raises,它不仅可以断言抛异常,还能捕获异常内容:

with pytest.raises(ValueError) as excinfo: divide(1, 0) assert "除数" in str(excinfo.value)

这个写法比try/except更好,因为try/except会吞掉异常,如果函数没有按预期抛错,你还要额外加else分支才能发现问题;而pytest.raises在异常没发生时直接让用例失败,语义更清晰。我还见过有人用assertRaises但没写上下文管理器,导致断言根本没生效,这种隐蔽问题全靠代码审查才能发现。

还有一个容易忽略的点:断言尽量不要只写assert True或不带条件。我们看过一些测试,里面只有一段打印和一个恒真断言,等于白测。测试的价值在于用“明确的预期”去对抗“未知的实现”,如果没有预期,跑一万次也发现不了问题。哪怕只是检查函数返回了非空结果,也比什么都不校验强。断言的粒度可以细一点,宁可多写几个 assert,也不要一个大杂烩断言盖住所有细节。

4. 进阶核心:用 fixture 管理环境和数据

4.1 为什么非要用 fixture 不可

很多测试需要前置条件:测试连数据库要建临时表、测试读文件要准备临时目录、测试调 API 要伪造返回值。如果这些前置逻辑散落在每个用例里,代码会立刻变得又长又重复。unittest 时代的解法是 setUp 和 tearDown,但问题在于它们只能按类组织,不能按需复用,一旦多个测试类需要同样准备逻辑,就得写两遍。

pytest 的 fixture 改变这个局面:它是一段可以被声明式使用的“准备逻辑”,测试函数只需要把 fixture 名当作参数放进去,pytest 就会自动调用它并注入结果。更强大的是 fixture 可以互相嵌套、有作用域控制、还能定义清理逻辑,形成一个清晰的依赖图。用生活里的例子来说,fixture 就像餐厅的后厨备菜:提前把菜洗好切好,客人点菜时直接下锅,效率高,也不会每个厨师各备各的。

4.2 用 fixture 搭一个带清理的环境

举个例子。假设被测函数依赖一个临时目录,需要在里面写入配置再读取。fixture 可以这么写:

import pytest @pytest.fixture def temp_dir(tmp_path): config_file = tmp_path / "config.ini" config_file.write_text("[default]\ntimeout=5\n", encoding="utf-8") yield tmp_path # 这里可以写清理逻辑,pytest 会在用例结束后执行

测试函数用起来:

def test_load_config(temp_dir): cfg = load_config(temp_dir) assert cfg.timeout == 5

tmp_path 是 pytest 内置 fixture,专门提供测试专用的临时目录,用例结束自动清空,不用自己管理路径。上面例子说明 fixture 的返回值可以直接注入测试,需要清理时用 yield 把它拆成两段:yield 之前是准备,yield 之后是清理。有些场景需要显式关闭数据库连接、删除临时资源,就可以放在 yield 后面。这种写法的好处是,清理逻辑跟准备逻辑放在一起,不会出现“setUp 里建了资源,tearDown 里忘了关”的错位。

你可能好奇为什么不用return tmp_path然后让系统自己回收。确实,很多临时目录可以让系统自动回收,但如果你的测试会打开文件句柄、创建子进程、写入中间产物,不及时清理会在长时间跑测试时积累垃圾,甚至触发跨用例干扰。所以,yield 清理不是可有可无的仪式感,而是必要的卫生习惯。

4.3 fixture 的作用域和共享策略

fixture 默认是函数级,每个测试函数都会执行一次。如果准备逻辑很重,比如启动一个测试数据库或加载模型,可以给它指定作用域:

@pytest.fixture(scope="module") def db_conn(): conn = create_db() yield conn conn.close()

scope 可以取 function、class、module、session。scope 越大,执行次数越少,但状态污染风险也越大。比如 module 级 fixture 返回一个会被修改的列表,前一个用例改了列表内容,后一个用例拿到的就是被改过的数据。我建议默认用 function 级,只有确实重量级的资源再提升 scope,并且要保证 fixture 内部是“只读”或“每次重置”的。

另外,多个测试文件都要用到的 fixture 可以放在根目录的 conftest.py 中。pytest 会自动识别 conftest.py 里定义的 fixture,不需要 import。如果你只在某个模块里定义 fixture,其他模块想用是不行的,这在项目分层后会是一个很常见的困惑。conftest.py 是共享 fixture 的集中营,但别把太多业务逻辑塞进去,否则维护起来也头疼。我更倾向只放跨文件的通用准备逻辑,把业务相关的 fixture 放在对应测试模块里,这样每个文件的职责一目了然。

5. 数据驱动测试:参数化解决重复劳动

5.1 多组输入反复测,怎么写才不累

测试函数的套路经常是“给定输入,断言输出”。如果同一个逻辑需要覆盖十组输入,最朴素的写法就是复制十次测试函数,只是参数不同。代码看起来很不专业,也难维护,改了一处期望值要改十处。更麻烦的是,如果只改了其中一组参数,其他九个容易忘记同步,测试之间就出现不一致。

参数化让一个测试函数自动展开成多个场景。pytest 的parametrize装饰器可以给函数注入多组参数,每条参数组合对应一个测试用例,失败时能清楚定位到是哪个参数组挂了。这有点像一个模板批量生成订单:文案框架一样,填充的数据不一样。

5.2 parametrize 的使用示例

沿用除法例子。我们想检查多组正常除法结果:

import pytest from core_math import divide @pytest.mark.parametrize("a,b,expected", [ (10, 2, 5), (9, 3, 3), (7, 2, 3.5), (-6, 2, -3), ]) def test_divide_with_params(a, b, expected): assert divide(a, b) == expected

跑一下会看到 4 个用例,名字会自动带上参数标记,比如test_divide_with_params[10-2-5]。如果某个场景失败,pytest 的 -v 输出里会显示具体参数,方便直接定位。实测下来,参数化测试最直观的好处就是“一个函数能当多个用例用”,维护时只改一处。

参数还可以配合 fixture 使用。比如 fixture 返回一个环境对象,参数化注入不同的配置字符串:

@pytest.fixture def env(tmp_path, request): name = request.param path = tmp_path / name path.mkdir() return path @pytest.mark.parametrize("env", ["dev", "prod"], indirect=True) def test_env_config(env): # env 此时指向 dev 或 prod 目录 ...

indirect=True 的意思是让参数名直接作为 fixture 名解析,这个技巧进阶用得比较多,初学可以先了解。它可以把“环境差异”变成测试矩阵的一维,让同一套断言跑在不同配置上,非常有价值。

5.3 参数化时应该覆盖哪些边界

不要只写“正常场景”。我建议每组参数化至少考虑:最常规的取值、边界值(最小值和最大值附近)、空值或 None、异常输入。还是以除法为例,除数接近 0 但不等于 0 的情况、除数为 0 的情况、被除数为非常大的数的情况,都是值得覆盖的。写参数化之前,先想想这个函数的自然边界在哪里。数据驱动的价值不在于数量多,而在于覆盖面广。

这里有一个反模式:把多个无关的行为塞进同一个参数化列表,导致测试失败时你还要判断是哪个语义出问题。所以参数化最好按“同一个业务规则下的不同数据”来组织,不要把所有断言都揉在一起。比如“除法计算结果”和“除数为零的错误提示”是两个行为,应该拆成两个参数化函数,否则一条数据失败时,定位问题的时间会变长。

6. 隔离外部依赖:mock 让测试又快又稳

6.1 为什么要隔离外部依赖

真实项目基本都有外部调用:HTTP 请求、数据库读写、消息队列、第三方 SDK。如果测试直接连外部系统,会遇到三类麻烦:速度慢、不稳定、不可控。一次网络超时就可能让整个测试失败,而这跟你的代码逻辑无关。很多开发者初学测试时都会遇到“明明没改代码,测试就是红”的情况,十有八九是外部依赖不稳定造成的。

mock 的核心思想,是用一个模拟对象替换真实外部依赖,让被测代码认为它在和真实系统交互,实际上返回的是我们预设的结果。这样测试变得确定、快速,也更安全。尤其在做一些涉及支付、发信、推送这类服务时,不 mock 一下就跑测试,既危险又费时。

但注意,mock 不是回避责任的工具,它针对的是“与本用例无关的外部依赖”。如果你 mock 掉了被测函数本身,那这个测试就没有任何意义。后面我会专门说这个度怎么把握。

6.2 用 pytest-mock 模拟一个外部 API 调用

先看一个待测函数:

import requests def fetch_user_name(user_id: int) -> str: resp = requests.get(f"https://api.example.com/users/{user_id}") data = resp.json() return data["name"]

测试时我们不能真的访问外部接口。用 pytest-mock 插件(实测下来很好用):

def test_fetch_user_name(mocker): mock_resp = mocker.Mock() mock_resp.json.return_value = {"name": "小A"} mocker.patch("requests.get", return_value=mock_resp) assert fetch_user_name(1) == "小A"

mocker.patch 会临时替换requests.get,测试结束后自动还原。mock_resp.json.return_value 设定了 json() 的返回值,于是 fetch_user_name 内部拿到 data["name"] 就是“小A”。整个测试完全不需要联网,速度飞快。

如果你不想依赖 pytest-mock,unittest.mock 里同样有 patch 装饰器和上下文管理器,写法类似,只是换汤不换药。pytest-mock 的好处是提供了 mocker fixture,不需要手动处理 patch 的恢复逻辑,更省心。而且mocker.Mock()是 unittest.mock 里的 Mock 类的别名,用法一致,学习成本很低。

6.3 mock 的副作用和调用校验

有些场景不是只返回一个值,而是第一次抛异常、第二次返回正常。可以用 side_effect:

def test_retry_logic(mocker): real_get = requests.get call_count = 0 def fake_get(url, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: raise requests.ConnectionError("timeout") return mocker.Mock(json=lambda: {"name": "小B"}) mocker.patch("requests.get", side_effect=fake_get) assert fetch_user_name(1) == "小B"

side_effect 可以是异常、列表或函数,它决定 mock 被调用时的行为。列表会按顺序返回值,函数则可以精确控制更复杂的逻辑。这个能力在测试重试逻辑、失败降级策略时特别重要。

另外,还可以断言 mock 是否被正确调用:

def test_fetch_called_with(mocker): mocker.patch("requests.get", return_value=mocker.Mock(json=lambda: {"name": "x"})) fetch_user_name(42) requests.get.assert_called_once_with("https://api.example.com/users/42")

这里的灵魂是“调用到了正确的 URL、正确的参数”。很多 mock 测试只关心返回值,却忘了校验调用参数,结果 mock 掩盖了参数拼接错误,测试照样变绿。所以测试里一定要花几行来断言“外部依赖确实按照预期被调用了”。

再说一下 mock 的度。网上观点很多,我的经验是:mock 边界要放在项目代码和外部系统的交界处。项目内部的工具函数、类之间协作,尽量用真实对象;只有跨到边界、面向不可控依赖时再 mock。这样测试能覆盖到我们真正写的逻辑,而不是把内部实现也架空。mock 太狠,测试可能绿得很虚假。比如一个内部函数明明是计算逻辑,如果你 mock 掉它的入参和返回值,那这个测试就只能测到一层壳,没有任何价值。

7. 覆盖率与测试反馈闭环

7.1 覆盖率不是唯一指标,但它是体检表

覆盖率(coverage)指的是测试运行时,多少比例的代码行被执行到。它不能证明逻辑是否正确,但能暴露“哪些代码从来没被运行过”。对于维护过一段旧代码又没测试的人,覆盖率报表是很吓人的:你会看到大量分支标红,说明它们已经很久没有接受过任何验证。

Python 生态里最常用的是 coverage 工具,也可以直接用 pytest-cov 插件。安装插件后,运行:

pytest --cov=core_math --cov-report=term-missing

输出里会列出每个文件的语句覆盖率、分支覆盖率和未覆盖行号。如果某些行长期飘红,要么是死代码,要么是你漏测了关键分支。这两种都需要处理,但力度不同。死代码可以直接删,漏测分支则要想办法补用例。

7.2 如何把覆盖率真正用起来

覆盖率不应该成为一个“百分比游戏”。不同模块的合理覆盖率差异很大:核心业务函数、解析器、对外接口,应该尽量高;别人看起来像脚本的一些 CLI 入口、调试代码,可以适当放宽。建议的做法是在核心模块设定一个最低阈值,比如--cov-fail-under=80,低于 80 就让测试失败,在 CI 里强制检查。但别把阈值一次性定到 100,那样会逼出大量只为了“覆盖”而写的空测试,反而没有意义。

另一个关键习惯:每写完一个测试,用覆盖率报告看一眼自己到底有没有覆盖到新写的分支。没有覆盖到的行,往往就是边界条件和异常分支。我见过很多开发者写测试时喜欢挑顺路径写,异常分支根本不覆盖,结果上线最怕的就是异常分支出错。覆盖率报告就像体检报告,你可以不追求满分,但至少要知道哪些指标常年偏低。

7.3 把测试接入日常流程

这里说的日常流程不一定是复杂的 CI 系统。最简单的方式是给项目加一个 Makefile 或脚本,把install,test,coverage等命令固化。以后每次提交前跑一遍,不绿就不推。如果用了别的协作工具,也可以在提交钩子里加一条命令。

CI 里跑测试时,建议不要只跑一次,还可以考虑分环境、分测试层级。单元测试、集成测试放在不同目录,用不同标记或路径来区分。我本人在做稍大一点项目时,都会在 pytest 里配置:

[pytest] testpaths = tests addopts = -q --disable-warnings --maxfail=5

addopts 里的--maxfail=5很有用:一旦连续失败 5 个用例,测试就停止,避免一堆错误信息刷屏,省时间。这也算是一个小技巧吧。如果你的项目还在成长阶段,先不用急着搭完整流水线,只要保证本地一条命令能跑全部测试,就已经成功了一半;后续再逐步加入覆盖率阈值和并行执行等项。

8. 常见问题与排查技巧实录

8.1 测试老是收集不到用例,为什么

最基础的原因有三个:文件名不以 test_ 开头、函数名不以 test_ 开头、测试文件不在 pytest 的搜索范围里。pytest 默认搜索当前目录下所有 test_*.py 或 *_test.py 文件。如果你把用例放在src/底下且没有加__init__.py或配置文件,也可能收不到。解决办法是先用pytest --collect-only -q看一下它到底收集了哪些用例,这个命令非常适合排查。

8.2 fixture 作用域导致的状态污染

曾经有个项目,两个测试函数共用一个 module 级 fixture,fixture 返回一个列表,第一个用例往里面 append 了一条数据,第二个用例拿到的列表已经包含那条数据,导致断言失败。这种问题很隐蔽,因为单独跑第二个用例又是通过的。排查时可以先加--setup-show看 fixture 的执行顺序,再思考共享数据是否被修改。最直接的解药是:除非有必要,否则 fixture 只用 function 级,并且所有 fixture 返回的对象尽量不携带可变状态。

8.3 mock 没有生效或 patch 了错误目标

mock 最常见的问题是 patch 错了地方。比如你看到代码里import requests,然后在测试里mocker.patch("requests.get"),但没有考虑到被测模块可能写的是from requests import get。如果你 patch 了全局的 requests.get,而被测代码用的是导入本地的 get,patch 就不会生效。正确做法是 patch 被测模块的命名空间,比如mocker.patch("core_math.requests.get"),这里的 core_math 是使用 requests 的模块名。

这个错误非常经典,很多初学者会困惑为什么测试还是发起了真实请求。我的经验是:调用 patch 时,路径名要指到“被测模块里的那个名字”,而不是指到第三方库的原始位置。这也算是一个入门期最容易被绕晕的细节。

8.4 测试之间互相影响,跑单个通过、跑全量失败

除了 fixture 状态污染,还有可能是测试文件之间的全局变量修改、环境变量修改、当前目录切换等。排查时可以先缩小范围:用pytest tests/test_a.py tests/test_b.py逐个组合,找到哪两个文件放到一起会互踩。然后检查是否有共享的临时文件、是否有模块级变量在 import 时被改变。必要时用monkeypatch.setenv等 fixture 来隔离环境变量,不要直接改 os.environ,因为改了以后不恢复会影响后面用例。

我遇到过最离谱的一次是某个模块通过os.chdir()切换了当前目录,后面所有依赖相对路径的测试全部失败。从那以后,我在测试代码里都会特别留意“有没有改变全局状态”的操作,能不用就尽量不用,如果非用不可,必须放到 fixture 里并保证 teardown 恢复现场。

8.5 排查工具快速速查

命令/参数用途
pytest --collect-only -q查看 pytest 到底收集了哪些用例
pytest --setup-show显示每个 fixture 的创建与销毁流程
pytest -x遇到第一个失败就停
pytest --lf只跑上次失败的用例
pytest -k "关键名"按表达式筛选用例
pytest --pdb失败后进入调试器

这些命令组合起来,能把排查效率拉高一截。特别是--pdb,新手可能不熟悉,但它比 print 调试好用得多,在失败现场直接查看变量,能节省大量来回打印的功夫。

关于单元测试,我最想强调的一点是:它不是一个“写一次就结束”的产物,而是一个随着代码演化不断修改的活文件。你重构一个函数,测试可能也要改;你新增一个分支,测试就要补上新分支。如果测试本身变得陈旧、不再与业务行为一致,那它不但没有价值,反而会给人虚假的安全感。所以,定期审视测试代码的维护成本和实际收益,也是一项需要练习的能力。

我个人的习惯是:先写最简单的正常路径测试,让它通过;然后马上补异常分支、边界值;每跑一次就打开覆盖率报告看一圈。长期下来,心里对代码的掌控感会强很多。如果你还没有建立测试习惯,不妨从今天这个除法例子开始,把第一个真正属于自己的测试用例跑及格,再慢慢扩大覆盖范围。这条路不需要一步到位,但值得每一步都踩实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询