1. 为什么是Pytest:它到底比unittest强在哪
先聊个现实问题,很多人都问过我:Python自带的unittest不是也挺好的吗,为什么要换pytest?
我在公司待过好几个项目,刚开始也是unittest一把梭。写一个测试用例,得继承unittest.TestCase,测试方法要写def test_xxx(self),断言得用self.assertEqual()、self.assertTrue()这些自带方法。如果一个测试类里十几个用例,每个用例还想做不同的前置准备,那个setUp()方法基本就是个大杂烩。更别提断言失败时,assertEqual打印出来的message信息,说实话有时候连我自己都要仔细看才知道到底差在哪里。
后来碰上接口自动化测试的项目,用例量动不动就上百,我咬咬牙切到了pytest。第一感受是写起来真轻,一个普通函数,里面一个assert就完事,不需要继承任何东西。但真正让我决定彻底换掉unittest的原因,是pytest的几个核心能力。
首先,断言。pytest直接用Python原生的assert语句,失败时自动展开表达式,告诉你左右两边到底谁是谁。比如assert result["code"] == 200,失败了你直接能看到result["code"]的实际值和200之间的差异,对于调试来说比assertEqual那种死板的输出强太多了。
其次,fixture机制。这是pytest的灵魂。unittest里用setUp和tearDown管理前后置,但作用域只有类级别,函数级别的临时数据、模块级别的资源、会话级别的登录token,你就得自己想办法组织。pytest的fixture按scope区分,可以精确控制到函数、类、模块、包、会话五个级别,而且还可以互相依赖、做参数化。这个能力放到接口自动化测试里简直就是刚需——登录token只需要做一次,数据库连接每个用例隔离,这些都是fixture顺手就能安排的。
再加上插件生态。pytest最恐怖的地方是插件数量和质量都相当能打。跑并发有pytest-xdist,生成高级报告有pytest-allure,统计覆盖率有pytest-cov,失败重跑有pytest-rerunfailures。这些插件随便挑一个都是"插上就能用",不需要你改任何测试代码。相比之下,unittest要自己做一套这些能力,得花不少额外功夫。
还有一点就是用例发现机制。pytest默认以test_*.py、*_test.py作为测试文件,test_开头的函数或方法作为测试用例,也可以自定义。这比unittest里必须用unittest.main()或TestLoader那一套灵活得多。目录一摆好,命令行一敲,用例直接全部收集上来,省心。
当然,我不是说unittest一无是处。对于一些非常简单的、项目里也就几十个用例的场景,用unittest也能正常运行。但一旦测试量上来,要搞数据驱动、前置后置、并发执行,pytest是性价比最高的选择。这篇分享就以我这两年在接口测试和单元测试中的实际经验为背景,把pytest的入门路径掰开揉碎,从怎么装、怎么写、怎么跑,到fixture机制、参数化、插件生态和常见坑,一步一步走一遍。
2. 环境准备与第一个用例
2.1 安装与目录规范
先解决环境。不管你是Windows、macOS还是Linux,只要Python环境正常,pytest的安装一条命令就能搞定:
pip install pytest装完之后验证一下版本:
pytest --version能看到版本号说明就绪了。如果你用的是虚拟环境,记得激活虚拟环境再装,尽量避免污染系统级Python。国内网络环境不好的话,可以加国内的pip镜像源,比如清华源:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pytest然后是项目结构。pytest的用例发现基于三个约定:测试文件命名、测试函数/方法命名、testpaths配置。默认情况下:
- 文件命名为
test_*.py或者*_test.py - 文件内的测试函数或方法以
test_开头 - 类名一般以
Test开头,且类内方法名为test_*
我习惯的项目结构长这样:
project/ ├── src/ │ └── ... ├── tests/ │ ├── test_user_api.py │ ├── test_order_api.py │ └── conftest.py └── pytest.initests目录单独放测试代码,业务代码放src目录,根目录放一个pytest.ini做统一配置。这样做的好处是测试代码和业务代码不互相污染,后续用pytest跑的时候只扫描tests目录,收集速度也快。
这里有个细节很多人会忽略:如果业务代码和测试代码混在一起,pytest默认会把测试文件所在目录加入sys.path,有时候会意外引入错误的模块路径。单独建tests目录再配合pytest.ini的testpaths,可以规避这类问题。
2.2 第一个测试用例
新建一个测试文件,比如test_first.py,写下最简单的用例:
def test_addition(): assert 1 + 1 == 2 def test_string_contains(): name = "pytest is awesome" assert "pytest" in name在终端输入:
pytest test_first.py你会看到类似输出:
collected 2 items test_first.py .. [100%]两个点表示两个用例都通过了。如果故意改一个断言,让它失败:
def test_addition(): assert 1 + 1 == 3再跑一次,输出里就会清楚地显示出assert (1 + 1) == 3,左侧的实际值是2,一目了然。这就是前面说的pytest的断言输出能力,直接告诉你哪里错了,而不是让你盯着一个笼统的False猜半天。
2.3 命令行参数
把这几个常用的运行参数背下来,日常就能吃得开:
pytest -v # 显示每个用例的详细结果 pytest -q # 简洁模式,只输出点和汇总 pytest -k "login" # 按用例名筛选,支持模糊匹配 pytest -m smoke # 按标记(marker)筛选,比如只跑冒烟用例 pytest -s # 显示print输出,调试时经常用 pytest --collect-only # 只看收集到哪些用例,不执行 pytest test_first.py::test_addition # 精确执行指定用例 pytest --maxfail=1 # 遇到第一个失败就停止刚开始用pytest的人容易犯一个错误,就是排查问题的时候不带-s,结果print内容全被吞掉,然后一脸迷茫。其实pytest默认为了保持输出整洁,捕获了print的输出,加了-s之后就全部放出来了。调试用例时我的常规操作是pytest -s test_xxx.py::test_yyy,先精确定位到一条用例再查问题。
另外,-k这个参数在用例多的时候特别救急。比如我只想跑所有包含order的用例,pytest -k "order"就够了,不用去改文件。如果想排除某个关键词,用-k "not order"。逻辑运算符也支持,比如-k "order or user"。
2.4 配置文件pytest.ini
新手往往不写配置文件,命令行参数一把梭,这也没错,但项目一复杂就不够用。推荐根目录放一个pytest.ini,把公共配置沉淀下来:
[pytest] testpaths = tests python_files = test_*.py python_functions = test_* addopts = -v -s markers = smoke: 冒烟用例 slowly: 慢速回归用例testpaths指定扫描目录,python_files指定测试文件匹配规则,addopts表示默认追加的命令行参数,markers则是注册自定义标记。注册标记是为了避免pytest在用例上用了未登记的marker时,报出Warning。这也是一个很多新手会遇到的提示:如果运行的时候看到PytestUnknownMarkWarning,说明你在用例上用了@pytest.mark.xxx但没在配置里登记,把marker写进pytest.ini就能消掉。
3. Fixture机制:pytest真正强大的地方
3.1 fixture与unittest的setUp/tearDown
先理解一个概念:fixture解决的是什么问题?
写测试时经常要做前置准备,比如开数据库连接、创建临时文件、登录拿token、往测试环境塞数据。等用例跑完,还要清理现场,比如关连接、删数据、恢复配置。在unittest里,你把它们拆成setUp和tearDown。但问题随之而来:如果不同用例需要不同的前置条件怎么办?如果某些用例需要登录,某些不需要呢?在unittest里你只能要么全都登录,要么做一堆if判断,代码越来越难看。
pytest的fixture思路完全不同。fixture是函数级别的"依赖注入",一个测试用例需要什么,就在参数里声明什么。pytest执行用例前,会先自动实例化对应的fixture,执行其前置逻辑,把返回值传给用例,跑完之后再执行后置清理逻辑。
看一下最经典的写法:
import pytest @pytest.fixture def user_token(): token = login("admin", "admin123") yield token logout(token)user_token这个fixture,yield之前的代码属于前置部分,返回值token会被注入到请求它的测试用例中;yield之后的代码在用例执行完之后执行,属于后置清理。测试用例想用就直接在参数列表里写user_token:
def test_get_user_info(user_token): headers = {"Authorization": user_token} ...这样每个用到token的用例都能拿到登录后的token,用不到token的用例根本不用声明它。可组合、可复用、可精准控制,这就是fixture的设计优势。
3.2 scope:控制fixture的生命周期
fixture通过scope参数控制生命周期,一共五个级别:
| scope | 生命周期 | 典型场景 |
|---|---|---|
| function | 每个测试函数执行前后各一次 | 临时数据、独立连接 |
| class | 每个测试类只执行一次 | 类级别的公共准备 |
| module | 每个测试模块执行一次 | 模块级配置 |
| package | 每个包执行一次 | 包级公共资源 |
| session | 整个测试会话只执行一次 | 登录token、全局配置 |
默认是scope="function",也就是每个用例都会创建一个fixture实例。如果你的了一个session级别的登录fixture,那么整个测试期间登录动作只会执行一次:
import pytest @pytest.fixture(scope="session") def session_token(): token = login("admin", "admin123") yield token logout(token)这类设计在接口自动化测试里尤其实用。每次接口用例执行前都去登录一遍,效率太低了,光是登录这个动作就要消耗不少时间;而session级别的token可以保证整个测试流程只要一次登录,后续用例全部共享。但要注意,session级别的fixture可能会保留状态,如果你用例间存在数据污染,排查起来会比较头疼。所以我的习惯是:共享的、只读的资源用session级,每个用例都要独立的、会变的数据用function级。
class和module级别的fixture我用的频率没有前两者高,但有一个场景值得提:如果你有一批用例属于同一个测试类,且它们共享相同的前置条件,那class级别的fixture比function级别效率高得多。我之前在数据库相关测试里,在一个测试类中每个用例都要读取同一个慢速磁盘文件,后来把fixture从function改成class,同样数量用例的执行时间直接砍掉一半。
3.3 自动使用:autouse
fixture加autouse=True后,不需要显式声明参数,pytest会自动执行这个fixture。适合做全局的、每个用例都需要的前置后置工作。
@pytest.fixture(autouse=True) def print_test_name(): print("开始执行测试") yield print("测试结束")autouse一个非常重要的应用场景是日志收集。我曾经在测试一个支付网关接口时,需要把所有用例的请求和响应都打到一个独立的日志文件里,方便排查线上问题。一个autouse的fixture,在yield前后记录时间戳、请求ID、用例名,所有用例都自动带上这套逻辑,还不用一个用例一个用例去修改。但autouse也不是随便乱用的,它对测试干扰较大。比如有些环境变量、mock状态在autouse里设置,很可能在无意中影响其他不相关的用例。我的建议是autouse只放那些"人人依赖、无人显式声明"的公共设施,比如日志、全局超时设置、case级的环境变量隔离。
3.4 conftest.py:公共fixture的家
如果一个fixture只在一个文件里用,直接写在该文件顶部就行。但如果有多个测试文件都要复用同一个fixture,就要把fixture放到一个叫conftest.py的文件里。pytest会从测试文件的目录层级向上查找conftest.py,也就是说父目录的conftest.py对子目录下的测试文件自动生效。
比如这套结构:
tests/ ├── conftest.py ├── test_user_api.py └── order/ ├── conftest.py └── test_order_api.py根目录的conftest.py可以放所有模块都需要的基础fixture,比如日志、测试环境地址;order目录的conftest.py放订单模块独有的fixture,比如订单数据的构造。这两个conftest里的fixture对各自的测试文件生效,互不干扰。
这里有个坑我踩过不止一次:conftest.py中的fixture名不小心和测试文件里的局部函数名重名了,结果pytest在解析用例依赖时,把局部函数当成了fixture来传,报出一堆奇怪的定位错误。后来我养成了一个习惯,所有fixture用统一的命名风格,用形如fixture_xxx或者xxx_fixture的方式区分普通函数。
4. 参数化与数据驱动:让用例从1条变N条
4.1 @pytest.mark.parametrize的基本用法
如果一个测试函数只有一组数据,那写成一函数就够了。但现实是,一条逻辑往往要验证多组数据,比如登录接口,要覆盖正确密码、错误密码、账号不存在、密码为空等场景。如果每个场景都单写一个函数,代码量会爆炸,维护成本也高。
参数化就是为了解决这个问题。用@pytest.mark.parametrize,一条测试函数就变成了多条测试用例:
import pytest @pytest.mark.parametrize("username,password,expected_code", [ ("admin", "123456", 200), ("admin", "wrong", 401), ("nobody", "123456", 404), ("admin", "", 400), ]) def test_login(username, password, expected_code): result = login_api(username, password) assert result["status_code"] == expected_code执行的时候pytest会生成4条独立的用例,每一条的测试数据不同。如果其中有失败,只有对应的那一条显示失败,其他照常运行。这在回归测试里特别好使,一次逻辑写三五行,几十组数据往列表里一塞,用例覆盖度立刻拉满。
参数名和参数值列表是成对出现的,也可以叠加多个parametrize装饰器实现笛卡尔积,比如验证不同用户在不同环境下的行为:
@pytest.mark.parametrize("username", ["admin", "guest"]) @pytest.mark.parametrize("env", ["dev", "staging"]) def test_user_profile(username, env): ...这样会生成2*2=4条用例。但叠加之后用例数量是乘法增长,如果组数多,用例数量会非常可观,尽量确认是不是真的需要全组合。
4.2 给用例起个好认的名字
参数化之后,pytest生成的用例名是test_login[admin-123456-200]这种形式。默认会直接把参数值拼上去,如果参数里有中文、特殊字符或者特别长的对象,用例名会变得异常难以阅读。
用ids参数可以自定义每个参数字段的展示名:
@pytest.mark.parametrize( "username,password,expected_code", [ ("admin", "123456", 200), ("admin", "wrong", 401), ], ids=["correct password", "wrong password"] ) def test_login(username, password, expected_code): ...这样可以一眼看出每条用例验证的是哪个场景,对排错帮助很大。尤其是跑pytest -v时,可读性会好很多。
4.3 从外部文件读取测试数据
当测试数据量非常大,比如接口自动化测试中的几十上百条用例数据,硬编码在测试代码里会让代码失控。比较常见的做法是把数据放在JSON、YAML或CSV文件里,测试代码负责读取并生成测试数据。
举一个JSON数据驱动的例子:
[ {"username": "admin", "password": "123456", "expected": 200, "desc": "正确密码"}, {"username": "admin", "password": "wrong", "expected": 401, "desc": "错误密码"} ]对应的测试代码:
import json import pytest def load_json_data(path): with open(path, encoding="utf-8") as f: return json.load(f) @pytest.mark.parametrize("case", load_json_data("./data/login_cases.json")) def test_login_from_json(case): result = login_api(case["username"], case["password"]) assert result["status_code"] == case["expected"]这种做法的好处非常直接:测试逻辑没有变,数据文件却可以随时增删。产品说"我又发现了一个边界场景",我直接往JSON文件里加一条记录,跑一遍pytest,用例自动多了一条,完全不用碰代码。
需要注意一点,参数化是在收集阶段就发生的,load_json_data在导入测试模块的时候就被调用了。如果文件路径写错或者JSON格式有问题,pytest收集用例时就会报错,这个报错信息有时候没有那么直观,需要靠报错堆栈定位。我自己遇到过一次,JSON文件末尾多了一个逗号,收集阶段一直报错,排查了半天才发现是数据文件格式问题。
4.4 数据驱动在接口自动化中的落地
数据驱动在接口自动化测试里最能体现价值。某个业务接口的参数组合非常多,比如订单查询接口可能有订单号、用户ID、时间范围、页码、排序方式等维度,每个维度又有若干边界值。我通常会把核心接口的测试数据独立成一个数据文件,每条记录包括请求参数和预期结果,然后用一条参数化用例统一执行。
这带来的额外收益是测试报告的可读性大大提高。结合后面要讲的allure报告,每一条数据驱动的用例都能单独显示,失败后一眼能看到是哪个参数组合出的问题,是产品逻辑bug还是测试数据过期,定位速度比旧式的"一个用例内部多次断言"模式快得多。
要注意的是,数据驱动的用例多了以后,如果每条用例都打真实的业务接口,执行时间会很长。这时候就需要结合pytest-xdist做并发,让多条case并行跑。数据文件的用例独立性就变得非常重要,如果case之间互相有状态依赖,并发后会出现偶发失败。所以,写数据驱动用例前,先思考一下这些数据是不是真的互相独立。
5. 插件生态:日常使用的六个真香插件
5.1 pytest-xdist:并发执行测试
用例多了之后,最直接的痛点就是一个一个跑太慢。pytest-xdist可以做到多进程并行执行测试用例。
安装很简单:
pip install pytest-xdist跑法更简单:
pytest -n 4-n后面的数字表示用几个进程来跑。也可以写成-n auto,它会根据机器CPU核数自动决定并发数。
我刚开始用xdist时犯过一个错误,直接把带session scope的fixture所谓的登录token共享给所有进程用,结果每个进程都去执行登录,后台的验证码校验直接被打崩了。后来看了官方文档才明白,xdist的并行机制是每个worker进程独立收集和执行用例,session级别的fixture在每个worker里都会执行一遍,它跟你预期的"全进程共享"根本不是一回事。如果你打算做并发测试,fixture里的前置准备要设计成每个worker可以独立完成的。把-n 4跑出来的结果跟单进程结果做对比,如果出现用例之间的状态干扰,多半就是并发导致的。
5.2 pytest-allure:让测试报告真正可读
对测试报告有要求的团队,应该都在找allure的方案。pytest搭配allure生成的报告,无论是测试步骤、失败堆栈还是历史趋势,都相当完善。
安装和基本用法:
pip install pytest-allure pytest --alluredir=./allure-results执行完之后需要打开报告:
allure generate ./allure-results -o ./allure-report --clean allure open ./allure-report在代码里可以用这些装饰器为用例增加更多上下文:
import allure @allure.feature("登录模块") @allure.story("验证用户密码") @allure.title("正确密码登录成功") def test_login_success(): ...feature基本对标模块名,story对标功能点,title则是最终展示在报告中的用例标题。报告中可以清晰区分模块层级,也便于管理层直接看大类的通过率。在失败用例的详情页里,pytest的断言信息、命令行输出、附带的日志都会一并显示出来。
有一点提醒一下,allure的报告结果不是你执行完pytest就自动生成的。--alluredir只是把json结果文件写到指定目录,必须再用allure命令来生成html报告。在CI/CD里要加上bulid报告这一步,否则不会自动产出网页。
5.3 pytest-cov:覆盖率统计
单元测试必须关注覆盖率。pytest-cov插件可以统计测试用例对业务代码的行覆盖率、分支覆盖率。
pip install pytest-cov pytest --cov=src --cov-report=html--cov=src告诉pytest统计src目录下代码的覆盖率,--cov-report=html会生成一份html格式的覆盖率报告。CI流水线里也可以加一个--cov-fail-under=80,如果覆盖率不足80%就直接失败。
用覆盖率主要是为了找测试死角。有时候写了一条测试把核心逻辑大部分分支都覆盖了,但仍然有某几行没被跑到。覆盖率的报告就像一张地图,标出哪几行代码还没被"走过",照着去补测试,效率比瞎猜高得多。
5.4 pytest-rerunfailures:失败用例自动重跑
网络抖动、环境偶发问题导致的用例失败,是最让人沮丧的,明明是稳定的代码,跑一次挂了,人肉确认后又没问题。pytest-rerunfailures可以针对这类偶发性失败做自动重试。
pip install pytest-rerunfailures pytest --reruns 3 --reruns-delay 1--reruns 3表示最多重试3次,--reruns-delay 1表示每次重试间隔1秒。但这里请务必想清楚,哪些用例可以重试,哪些不该重试。数据写入类、状态变更类的用例,重试很可能会造成重复数据或者脏数据。我更推荐把它用在纯读取类的接口测试,比如查询接口、详情接口,这些接口偶发失败通常都是网络抖动或服务端瞬时压力导致,重试一下无伤大雅;而对于下单、支付、创建订单这类操作,重试带来的副作用可能比失败本身更大,宁可让它失败后人工介入。
5.5 pytest-html:轻量级HTML报告
如果不愿意引入allure那么重的方案,pytest-html是很好的替代品。
pip install pytest-html pytest --html=report.html生成的是一个单文件HTML,打开就能看到所有用例的通过率、执行时间、失败原因。它虽然没有allure那么炫酷,但胜在轻量,一条命令就能出报告,特别适合个人项目或者小型团队临时验收用。
5.6 pytest-ordering:控制用例执行顺序
默认情况下pytest按文件、函数定义顺序执行,但有时候我们希望某些用例必须先跑,比如先准备数据再跑查询用例。pytest-ordering插件允许给用例加优先级。
import pytest @pytest.mark.run(order=1) def test_setup_data(): ... @pytest.mark.run(order=2) def test_query_data(): ...坦白讲,我对这个插件的态度是谨慎的。过度依赖用例顺序会让测试套件变得脆弱,因为一旦某个前面的用例挂了,后面的用例全都会跟着挂。它更适合用在必须的前置依赖,比如"先创建用户再验证用户详情",而不是用来作为测试的逻辑编排工具。能设计成独立用例的,尽可能不要用order依赖。
6. 常见问题与排查技巧实录
6.1 fixture解析失败:fixture不存在或参数混乱
报错信息形如fixture 'xxx' not found。这种情况十有八九是fixture定义的位置不对,或者参数名拼写错误。排查顺序:
- 先确认fixture是否定义在conftest.py中,并且conftest.py的目录层级是否覆盖当前的测试文件
- 再确认fixture名是否被局部同名函数覆盖
- 最后看一眼fixture的参数名是否跟测试函数的参数名一一对应
还有一个常见误会:测试函数里的参数是普通数据(比如字符串、数字),本来应该用参数化传入,结果不小心在函数参数里写了个普通变量名,pytest会把这个普通变量名当成一个fixture名去解析,自然就报fixture not found。解决方法是把数据交给参数化机制,不要把普通数据放在函数的形参里。
6.2 断言失败但不知道怎么调试
pytest默认的输出已经比unittest友好很多,但遇到复杂的断言,比如比较一个包含多层的字典,光看失败信息可能还不够。我常用的办法是:
- 加上
-s,让print输出可见,在断言前把关键变量打印出来 - 使用
-v,查看每个用例的完整信息 - 在用例内加上
pytest.set_trace(),配合--pdb可以在断言失败时进入调试器
具体操作:
pytest test_xxx.py -s -v如果用例里需要断点调试,可以在代码中直接写:
def test_complex(): result = get_complex_data() pytest.set_trace() assert result["data"]["items"][0]["price"] == 10运行到该行时,会进入PDB交互界面,可以逐行查看result的内容,想查哪一层就查哪一层。但是注意,用pytest.set_trace()调试完一定要删掉,否则CI里跑用例会卡在调试交互界面。
6.3 用例之间有相互依赖,导致随机失败
这个问题在数据驱动、并发执行时最容易出现。比如一个用例创建了订单A,另一个用例去查询订单A,后者的执行依赖前者执行成功。如果两个用例被分到不同的worker进程并行跑,后者可能先跑,订单A还没创建,断言直接失败。
方案有几个:
- 用fixture把"创建订单"和"查询订单"合成一个完整流程,在同一个用例里完成
- 用scope="class"或"module"的fixture把关联操作放在一起
- 使用xdist的
--dist=loadscope,让同一个测试文件里的用例尽量在同一个worker里执行
我个人的经验是:依赖关系应该在测试设计阶段规避,而不是靠工具硬调。如果一堆用例之间存在隐性的先后依赖,说明测试粒度设计得还不够好。尽量让每一条用例独立构造自己的数据,跑完再清理,这是最稳定的方式。
6.4 环境配置在不同机器上表现不一致
在Jenkins、GitLab CI或者GitHub Actions上跑用例,经常会出现本地全绿、CI上却有失败的"玄学"。这类问题多数出在环境变量、时间、时区、数据库状态和路径上。建议在conftest.py中做一个fixture,专门检查CI的必要环境变量,缺失直接fail fast,节省排查时间。
另外,测试中会用到外部资源的路径,建议用相对路径而不是绝对路径。去CI上一跑,容器的工作目录跟你本机不一样,路径一错,用例全部失败。用pathlib.Path(__file__).parent这种由测试文件自身推导路径的方法,可以避免大部分路径问题。
6.5 想跳过某些用例
场景:某个功能需要在生产环境验证,但当前是在测试环境,不需要执行。pytest提供了skip与skipif两个装饰器:
import pytest @pytest.mark.skip(reason="该用例仅在生产环境执行") def test_production_only(): ... @pytest.mark.skipif(env == "dev", reason="开发环境不支持") def test_pay_callback(): ...也可以使用条件判断的方式,在测试环境不是staging时直接跳过。这样不会把用例删掉,也不会执行报错,报告里能看到一条Skipped的记录,方便了解哪些用例没跑以及为什么没跑。
6.6 测试用例太多,收集阶段变慢
pytest收集用例时,会把匹配到的所有测试文件全部导入并解析。如果项目规模大,或者某些测试模块内部做了繁重的初始化工作,收集阶段就会很慢,跑一条用例也要等半分钟。
优化手段:
- 在pytest.ini中精确指定
testpaths,不要让pytest扫描整个项目目录
[pytest] testpaths = tests减少测试模块顶部的重活,比如不要在模块顶层就初始化数据库连接或加载大模型数据,把这类工作挪到fixture里,等真正需要用的时候再初始化
用
-p no:cacheprovider关闭缓存机制,在某些场景下也能提升一点点收集速度
我在一个大项目中碰到过收集一次要四十多秒的情况,最后发现是一个测试模块顶部调用了超时时间很长的网络请求。把那个请求挪到fixture里懒加载之后,收集时间瞬间降到了两秒。
最后的实践建议
从我个人的实际项目经验来看,pytest的入门曲线是比较平缓的,但真正用好它,靠的是把fixture作用域理清、参数化数据驱动落地、插件选型贴合项目场景,以及养成"用例互相独立、不依赖执行顺序"的设计习惯。前期图省事写的耦合用例,后期都会变成随机失败的定时炸弹,还不如一开始就花点时间组织好测试结构。
如果你是刚接触pytest,建议从一个小模块开始,把unittest的老用例迁移过来,对比一下旧代码和新代码的维护成本差异,很快就能体会到我说的"优雅"在哪里。等对这个框架的脾气摸熟了,再把数据驱动、并发执行、allure报告这些能力逐步加进来,你的测试体系会越来越像一个真正能扛住回归压力的工程体系,而不是一堆随手写的断言脚本。