☰
Pytest入门:从Unittest痛点走向优雅自动化测试
2026/10/11 6:26:05 网站建设 项目流程

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-html

2.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. Fixture:Pytest的灵魂

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"] >= 18

user_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(接口测试)、ui(UI测试)、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_login

parametrize产生的用例,节点ID里包含参数值,你甚至可以精确到某组参数。这种从粗到细的执行控制,在处理失败用例复现问题时特别高效。我通常的排查流程是:全量跑一遍,收集失败列表,然后逐个用pytest 文件::用例 --pdb的方式进入调试模式定位问题。等下,--pdb这个参数就是用例失败时自动进入Python调试器,配合断言信息和断点,能把定位问题的速度提升好几倍。这个技巧强烈建议你记住,排查问题的时候真的会救你命。

5. 断言与异常测试的进阶玩法

5.1 不仅仅是Equal:pytest的内建断言工具

日常测试中我们更多需要的是"验证某个行为是否符合预期",而不仅仅是比较两个对象是否相等。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(..., indent=2, sort_keys=True)先把两个对象格式化输出到日志里,肉眼对比起来更方便。这种土办法在某些时候比看断言信息还直观。

6. 插件生态:让测试进入工业化阶段

6.1 pytest-html:生成漂亮的测试报告

测试跑完没报告,等于白测。虽然命令行输出已经足够观察,但在团队协作和CI回执中,一份结构清晰的HTML报告是刚需。pytest-html插件生成报告的方式极其简单:

pytest --html=report.html --self-contained-html

--self-contained-html参数会把CSS和JS都内嵌到文件中,这样报告可以单独作为附件发送,不依赖网络也能在浏览器正常渲染。报告里会展示每个用例的执行时间、状态、失败原因,还有按模块和标记分类的汇总信息。我在项目中通常把它作为CI流程的默认产物,每次流水线结束都能自动生成并归档,方便回溯每一次构建的测试情况。

也有团队选择Allure报告,相比pytest-html,Allure的颜值和交互确实更胜一筹,但需要额外部署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到1

7.1 项目结构设计

理论讲再多,不如一个完整的案例来得直观。我带大家做一个简单的用户信息接口测试,假设后端提供了一个RESTful API:

GET /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客户端fixture:

import pytest from utils.api_client import ApiClient @pytest.fixture(scope="session") def base_url(): return "http://test-server:8000/api" @pytest.fixture(scope="session") def api_client(base_url): return ApiClient(base_url) @pytest.fixture() def new_user_payload(): return {"name": "test_user", "age": 18, "email": "test@example.com"}

注意scope="session"的作用——同一个测试会话内,该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 --html=report.html --self-contained-html --cov=utils --cov-report=html

--cov=utils表示统计utils包的覆盖率,--cov-report=html生成HTML格式覆盖率报告。如果你的CI流程需要覆盖率门槛,可以在pytest.ini中配置:

[pytest] addopts = --cov=utils --cov-fail-under=80

这样覆盖率低于80%,测试流程就会直接失败,强制团队保证测试质量。我刚给团队引入这个指标时大家怨声载道,但跑了两个迭代后,接口质量确实肉眼可见地提升了。覆盖率不是万能的,但没有覆盖率底线,测试很容易流于形式。

8. 常见问题排查实录与避坑指南

8.1 fixture作用域理解错误引发的诡异问题

我接手过一个项目,测试偶发失败,排查了很久发现是某个fixture被设置成了scope="session",其中保存了一份"当前用户ID"的变量,多个用例对它进行读写,产生了隐性的跨用例状态依赖。前面用例写进去的值,改变了后面用例的执行结果。这种问题最坑的地方在于:单独跑某个用例是好的,按顺序全量跑也是好的,但只要调整用例顺序或者跑特定的子集,问题就出现。

解决思路是:fixture返回的数据如果会被修改,尽量使用函数级作用域;如果确实需要会话级共享,也要确保共享的对象是不可变的,或者每次使用前做深拷贝(copy.deepcopy)。这里的核心原则是fixture作用域越小,状态污染的风险越低;作用域越大,性能收益越高——两者取平衡,而不是一味求性能。我在项目里基本只有连接客户端这种"无状态或低状态"的对象才用session级。

8.2 用例命名不符合规则导致静默跳过

pytest的用例发现规则是:文件匹配test_*.py或*_test.py,类名以Test开头且不包含__init__方法,函数名以test开头。很多时候你辛辛苦苦写了用例,运行完发现"collected 0 items",大概率就是这三个条件没满足。

这里有一个容易忽略的细节:如果你定义的类不继承任何东西,且类里面的方法名叫test_xxx,pytest会正常收集。但如果你在类里定义了setup_method这类unittest风格的方法名,pytest也会支持,不过为了统一风格我会直接删除,全部改用fixture。还有一点:测试类不能用__init__方法,pytest不会收集任何定义了__init__的测试类。这个坑看着小,但几乎每个新手都会踩一次。

8.3 断言失败后的定位技巧:--pdb和-trace

用例失败不可怕,可怕的是不知道怎么快速定位。我推荐的排查流程是:先看pytest给出的断言对比信息,一般问题就明确了;如果信息不够,用pytest <用例节点> --pdb在失败点自动进入调试器,检查当时的变量值、调用栈。进入pdb后可以用p打印变量、用w查看调用栈、用u/d切换栈帧,非常灵活。

还有一个参数是--trace,它会在每个用例开始执行时就进入调试器,适合需要逐步跟踪执行过程的场景。另外我觉得--maxfail=1也很实用,默认pytest会跑完全部用例才汇总,但如果用例很多、你只是想改一个bug,让它在第一个失败用例处停下来可以节省时间:

pytest --maxfail=1

8.4 不要在测试里依赖用例执行顺序

pytest默认按照文件内的定义顺序执行用例,但这是实现细节而不是语言保证。特别是当你用了xdist并行后,用例执行顺序完全不可控。一个优良的测试体系,必须有"测试之间互不依赖"的自觉——每个用例都应该能独立运行、独立校验、独立清理。

我在代码评审中最常打回的问题之一,就是某个用例依赖前面用例创建的测试数据。正确的做法是:每个用例自己创建所需数据,用完自己清理,或者至少确保清理逻辑不会影响其他用例。这就回到了fixture的威力——把"准备数据"和"清理数据"都封装在fixture里,每个用例独立调用,天然就隔离了。

另外,如果你发现自己的用例非得按顺序跑才能通过,不要急着给pytest加--strict或者依赖排序插件,先回头审视用例设计是不是出了问题。这往往不是框架的问题,而是测试设计的问题。记住一句话:不稳定的测试比没有测试更可怕,因为它会耗费团队大量的信任成本。

8.5 慢用例的排查与优化

有时候跑了很久,最后发现大部分时间耗在几个"巨慢"的用例上。pytest的--durations=10参数会显示执行时间最长的10个用例,这是一个非常好用的性能诊断工具:

pytest --durations=10

拿这个数据,优先优化那几个耗时大户。常见的手段是:把一次性初始化提升到session级fixture、用缓存减少重复网络请求、把耗时的外部调用替换成mock。我见过最离谱的情况是一个登录用例因为每次都重新建立数据库连接,耗时8秒,全量测试跑了300个这种用例就是40分钟。后来把数据库连接提升到session级,线上回归时间直接砍半。

我个人在实际操作中的一个体会是:pytest的价值并不仅仅在于"写起来少几行代码",而在于它把测试的思维方式理顺了。写测试不再是为了交差,而是成了一种跟代码对话的方式——你通过断言告诉程序"你应该是什么样",程序用通过或失败告诉你"你当前是什么样"。当你习惯了这种对话方式,你对代码的信心和对改动风险的掌控感都会完全不一样。如果有条件,把这些小技巧先用在一个不那么紧急的项目上一两个迭代,你会慢慢感受到"优雅"到底意味着什么。

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

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

立即咨询