做接口自动化有一段时间了,大部分团队一上来就是写用例、封装断言、接入 CI,很少有人会花时间认真对待项目根目录下那个pytest.ini文件。我一开始也觉得它无非就是放几个简单的命令行参数,直到有一次接手一个项目,用例目录混乱、标记满天飞、命令行参数十好几行,才意识到配置文件这东西前期不用心,后面全是账。这篇文章就把我在接口自动化项目里实际使用pytest.ini的完整经验拆开讲一讲,包括每个配置项到底做什么、怎么选、怎么组合、踩过哪些坑,希望能帮你把测试框架的底座打稳。
1. 为什么接口自动化框架里必须有 pytest.ini
先说一个最直观的场景。你写了几百条接口用例,散落在api_case、testcases、cases等不同目录下,每条用例都带着@pytest.mark.smoke、@pytest.mark.regression、@pytest.mark.deploy之类的标记。假设你现在想跑testcases目录下所有标记为 smoke 的用例,并且要带详细日志、跳过告警输出,命令行大概率长这样:
pytest testcases -m smoke -v -s --tb=short --disable-warnings --strict-markers --alluredir=./report/allure_results这个命令确实能跑,但问题很明显:参数太长、容易输错、团队成员每个人输入的参数不一致、新同事根本不知道要加--strict-markers或者--alluredir。时间久了,大家每人一个跑法,回归结果无法统一,排查问题全靠猜。
pytest.ini的核心价值就是把这类“约定”固化下来。它是 pytest 默认读取的配置文件之一,优先级最高,一旦存在,pytest 启动时就会先加载它,把里面所有配置项应用到全局。上面那串复杂的命令行参数,完全可以化为几行配置,之后所有人只需要执行pytest三个字母,行为就完全一致。
另外还有两个容易被忽视的功能。第一是标记管理。接口测试里smoke、regression、slow、dependency这些标记如果不注册,pytest 运行时会给出PytestUnknownMarkWarning告警,量一多日志就很脏。虽然这种告警不中断执行,但配合--strict-markers时,未注册标记会直接报错,让你从源头规避拼写错误。第二是测试用例的发现规则。pytest 默认递归查找文件名以test_*开头(或*_test.py结尾)的文件,但接口项目里经常有api_utils.py、test_data.py、helpers.py这类非用例文件,如果不在配置里限定testpaths,pytest 就会全盘扫描,非常影响收集速度,还可能误加载不该加载的模块。
从项目协作的角度看,配置文件就是团队的“公共契约”。新人来了,看一眼pytest.ini就知道整个项目怎么跑、有哪些标记、哪些目录被排除、测试数据和报告输出到哪,而不是到处找人问。我自己的经验是:一个接口自动化项目的pytest.ini一旦配置到位,后期几乎不用再动,真正是一劳永逸的事情。
2. pytest.ini 与其它配置文件的选型对比
pytest 官方支持四种配置文件:pytest.ini、pyproject.toml、setup.cfg、tox.ini。很多人会纠结该用哪个,我直接给结论:接口自动化项目优先选 pytest.ini,除非没有选择余地。
几种配置文件的核心差异如下:
| 配置文件 | 优先级 | 适用场景 | 注意点 |
|---|---|---|---|
| pytest.ini | 最高 | 纯测试项目,或只需要配置 pytest 的普通项目 | 文件编码必须保存为 UTF-8,否则中文注释容易乱码 |
| pyproject.toml | 中 | 项目本身是 Python 包,或使用 Poetry、PEP 621 管理依赖 | pytest 配置要写在[tool.pytest.ini_options]段落,字段名略有不同 |
| setup.cfg | 低 | 项目需要打包发布,且还在使用 setuptools 配置 | pytest 配置写在[tool:pytest]段落,且某些参数在旧版本上解析有问题 |
| tox.ini | 低 | 使用 tox 做多环境测试或 CI 矩阵 | pytest 配置写在[pytest]段落,通常与 tox 环境配置混在一个文件里 |
选pytest.ini的理由很简单:它解析规则最简单、官方支持最完整、没有额外的格式约定。比如pyproject.toml里要写[tool.pytest.ini_options],值必须符合 TOML 格式,字符串要用引号包起来,布尔值不能写true名字和 Python 里不一样,容易写错;setup.cfg和tox.ini更是有历史包袱,某些配置项在它们上面只做“尽力支持”,解析逻辑不同。你既然把精力花在接口测试上,就不要再为配置文件的坑分心了,直接用pytest.ini最稳妥。
还要特别强调一下,pytest.ini只要存在,就会覆盖其它所有配置文件的 pytest 部分,不需要担心和其它工具冲突。比如你项目用了setup.cfg管理代码风格,pytest.ini只管 pytest,两者互不干扰。这一点在大型项目里很重要,测试配置和工程配置可以彻底分离,出问题也容易定位。
还有一个容易踩的坑:pytest.ini的文件名必须严格叫pytest.ini,不能叫pytest.int、pytest_conf.ini,改一个字母都不行。有些同学会顺手建一个pytest.ini.txt,本质上是文本文件,虽然双击打开内容一样,但 pytest 根本不认,配置无效还没有明显提示,排查起来很让人崩溃。
3. 核心配置项逐项拆解与实操详解
现在进入正题,把pytest.ini里我认为接口自动化场景下最有用的配置项逐个讲清楚。每个配置项我都会说明它的作用、使用场景、我实际用过的配置写法,以及需要注意的细节。
3.1 testpaths —— 精准定位测试用例目录
接口自动化项目的用例目录通常不止一个。有的是按业务线分,比如testcases/order、testcases/pay、testcases/user;有的是按测试类型分,比如tests/api、tests/integration。如果不加testpaths,pytest 默认从当前目录下递归搜索,结果就是一个指令扫描了所有文件,速度慢不说,很容易把test_data、utils目录里名字里带 test 的文件也当成用例。
我的做法是在根目录统一建一个testcases目录,然后配置:
[pytest] testpaths = testcases这样 pytest 只会去testcases下找用例,其它目录一概不看。如果用例分布在多个目录,可以用空格分隔:
testpaths = testcases/api testcases/integration有一个细节需要注意:testpaths配置的路径必须相对于pytest.ini所在目录。很多人喜欢在testpaths里写带根目录的绝对路径,比如/home/user/project/testcases,这会让配置失去可移植性,换个环境就要改一次。正确做法是直接用相对路径,pytest 会自动基于配置文件所在目录解析。
我见过一个真实案例:同事把testpaths配成了testcases/,带了个斜杠,然后 pytest 偶尔收集不到用例。查了源码发现路径解析时,末尾斜杠在部分操作平台下会导致目录拼接异常。虽然 PyPI 最新版修了这个边界,但建议还是尽量别加多余的斜杠,保持路径干净。
3.2 markers —— 标记体系与严格校验
接口测试里标记是管理用例优先级和业务场景的强大工具。一套合理的标记体系通常包括:
- smoke:冒烟用例,核心链路,每次提测必须跑
- regression:回归用例,全量执行
- slow:耗时长的用例,定时任务里跑
- deploy:线上环境部署后的验证用例
- p1 / p2 / p3:用例优先级,按重要程度分层
如果不注册这些标记直接使用,pytest 运行时会给出黄色告警。之前我总觉得无所谓,直到有一次某成员手滑把@pytest.mark.smoke写成了@pytest.mark.somke,由于这个标记从未注册,pytest 只是提示 unknown mark,不会报错,结果所有 smoke 用例没跑成功,而 CI 日志里这种警告早就被刷掉了,问题被带到了线下环境,排查了半天。
解决办法是把标记注册进pytest.ini,并开启严格校验:
markers = smoke: 冒烟测试,核心链路用例 regression: 回归测试 slow: 耗时超过30秒的用例 p1: 一级优先级,阻塞性问题 p2: 二级优先级,功能性缺陷 p3: 三级优先级,一般性问题然后配合命令行参数--strict-markers,或者直接在 addopts 里加上这个参数。
addopts = --strict-markers开启之后,任何未经注册的标记都会直接抛出异常,用例根本不会执行。你看,这才是真正解决问题的方案。它还强制团队所有成员使用统一标记,避免各写各的。比如有人想加一个@pytest.mark.high的标记,如果没同步注册,跑用例的时候立刻报错,而不是悄悄地变成一条不执行用例。
标记名里冒号后面的部分是标记描述,接口测试中建议把描述写清楚,比如明确这个标记适用哪个环境、对应哪个测试阶段。这样在生成 Allure 报告或者 CI 里筛选时,别人也能看懂这个标记的用途。
3.3 addopts —— 把固定参数写进默认值
addopts是我最常用的配置项。它相当于给 pytest 命令注入一串默认参数,相当于命令行里那些每次都要敲的参数,现在统一在这里写一次。
一个典型的接口自动化项目配置长这样:
addopts = -v -s --tb=short --strict-markers --disable-warnings --maxfail=5逐个解释:
-v:详细输出,每个用例的执行结果都会显示。-s:允许 print 输出,接口测试里经常要打印请求体、响应体,不加这个参数 print 会被吞掉,调试时很恼火。--tb=short:发生断言失败时只输出短路信息,避免报错堆栈刷屏。--strict-markers:强制校验标记,前面已经说过。--disable-warnings:忽略 Python 警告,接口测试里常见的DeprecationWarning、UnverifiedHttpsWarning非常多,开着会干扰视线;注意这个参数不影响strict-markers的检查,两者可以并存。--maxfail=5:连续失败 5 条用例后终止执行,适合在调试阶段快速反馈,避免几百条用例全跑完才发现问题。
一个很容易被忽视的问题是,addopts里的参数是“默认值”,如果命令行里带了相同选项,命令行会覆盖它,但某些情况下是“合并”而不是“覆盖”。最典型的是重复添加多个参数时,比如配置里已经有-m smoke,命令行又写pytest -m regression,这种情况下后者会覆盖前者,不会同时跑两个。而像-v和-s这类开关型参数,命令行是没法“关掉”的,也就是说你配置了-s,任何人想在命令行关闭 print 输出都做不到,这套规则需要让团队成员知道。
还有一点,千万别在addopts里写环境相关的路径,比如--html=./reports/2024-08-01,这种参数应该在 CI 里动态生成后再追加,写死在pytest.ini里会导致测试报告永久覆盖到同一个位置。如果你确实需要默认指定报告目录,可以结合 Allure 的--clean-alluredir参数,每次执行前清空旧报告,避免结果混淆。
3.4 norecursedirs —— 排除目录,避免误收集
norecursedirs其实不太起眼,但接口自动化项目里作用很大。默认情况下 pytest 会忽略.git、__pycache__、venv、site-packages等常见目录,但你的项目里可能还有别的目录不该被收集,比如:
norecursedirs = utils common scripts reports uploaded_files这里utils和common是工具类目录,里面经常有test_connection.py这类文件名,如果不排除,pytest 会把它当成测试模块并尝试收集内部函数。更麻烦的是scripts目录,一些部署脚本、数据初始化脚本也可能用 test 做前缀,一旦被收集,运行时会报错,整个测试流程被中断。
配置排除目录需要注意,它做的是“递归排除”,也就是只要某文件名匹配,pytest 就不会进入该目录递归扫描。路径规则支持 glob 模式,比如norecursedirs = *.egg可以排除所有 egg 目录。但在接口项目里,建议大家直接用目录名白名单,简单可读,不要玩花活的通配符,降低维护心智。
顺带提一句,如果你把testpaths限制住了,其实norecursedirs在多数情况下不会触发。但两者是配合关系,testpaths是“只在这些目录里找”,norecursedirs是“即使在这些目录里也不要进去”,有重叠才是合理的防御性配置。我自己一般两个都会写,万一以后有人改了 testpaths,把scripts加进去了,norecursedirs还能兜底。
3.5 log_cli —— 实时观测与日志配置
接口自动化调试阶段最烦的事情就是用例失败以后,看不到请求参数和响应体。有些人喜欢在用例里加一堆 print,然后配合-s输出,但 print 的问题是它不属于结构化日志,而且被-s开关控制,在正式环境里策略很难统一。
pytest 内置了log_cli配置,开启后可以实时在终端显示日志:
log_cli = true log_cli_level = INFO log_cli_format = %(asctime)s [%(levelname)s] %(name)s: %(message)s log_cli_date_format = %Y-%m-%d %H:%M:%S这样在用例里用logging.info("请求响应数据: %s", resp.text)写日志,运行时命令行就能实时看到。相比 print,logging 可以分级别控制,正式回归时设为 WARNING 或 DEBUG,调试时设为 INFO,不用改代码。
注意,log_cli_level是全局的,但你可以通过caplog在用例级别调整。比如某些关键用例,我想临时看 debug 级别的日志,可以在用例内部用caplog.set_level(logging.DEBUG)。这一点在实际排查问题时特别有用,不需要为了单条用例重启整个执行流程。
log_cli_format里的格式稍微解释一下,%(name)s默认输出的是root,如果你在用例模块里设置了logger = logging.getLogger(__name__),它就会显示模块名,定位问题时一眼就能看出日志来自哪个模块。接口自动化项目里,强烈建议每个模块都显式创建 logger,不要用logging.info裸调用。
3.6 其他值得使用的配置项
除了上面几个大头,还有几个小配置项我平时也会用,放在一起说:
minversion:指定 pytest 的最低版本。接口自动化项目可能依赖pytest-assume、pytest-rerunfailures这类插件,它们对 pytest 版本有要求。配置minversion = 7.0后,如果环境里的 pytest 版本低于 7.0,会直接提示升级,避免插件运行时出现莫名其妙的不兼容问题。required_plugins:列出必需插件。这个配置在团队协作中很有用。比如项目依赖pytest-html、pytest-ordering、pytest-rerunfailures,如果某个同事换了新环境忘记装某个插件,pytest 启动时会直接报错提示缺少插件,而不是运行到一半才遇到fixture 'xxx' not found之类的诡异错误。xfail_strict:当配置为 true 时,标记为@pytest.mark.xfail的用例如果意外通过,会变成失败。接口测试里有些已知问题用 xfail 标记临时跳过,但如果别人修好了问题而忘记移除标记,用例就会直接报失败提醒你,这对保持测试结果真实性很有帮助。console_output_style:控制终端输出样式,如progress或classic。CI 里我一般配置为progress,大量的用例跑进度条更省日志空间;本地调试时用classic更清晰。这个配置在大型项目里值得关注。
4. 接口自动化实战:一份可复用的 pytest.ini 模板
这一章我直接给出一份我在接口自动化项目里使用的完整模板,并逐段解释每个配置的意图。你可以直接复制粘贴,在当前项目基础上调整路径和标记。
4.1 完整模板示例
[pytest] minversion = 7.0 testpaths = testcases norecursedirs = utils common scripts reports logs data required_plugins = pytest-html pytest-ordering pytest-rerunfailures # 命令行默认参数 addopts = -v -s --tb=short --strict-markers --disable-warnings --maxfail=10 --alluredir=./report/allure_results --clean-alluredir # 标记注册 markers = smoke: 冒烟测试,核心链路 regression: 全量回归 slow: 耗时超过30秒 p1: 一级优先级,阻塞问题 p2: 二级优先级,功能缺陷 p3: 三级优先级,优化建议 skip_deploy: 跳过布署环境用例 # 日志输出 log_cli = true log_cli_level = INFO log_cli_format = %(asctime)s [%(levelname)s] %(name)s: %(message)s log_cli_date_format = %Y-%m-%d %H:%M:%S # 告警过滤 filterwarnings = error:.*unclosed.*:ResourceWarning ignore::DeprecationWarning动一个细节:addopts采用了多行书写的方式,值由空格分隔,但为了可读性可以换行。pytest 会合并所有行内容当成一个字符串来处理,注意不要加多余的缩进,缩进在行首会被保留,造成参数解析混乱。我一般保持每行顶格写,或者统一四个空格缩进,但不要有的有缩进有的没有。
filterwarnings这行需要多说一句。它和标记类似,也是可以配置多条规则的。接口自动化里最常见的告警来自 requests 库未校验 HTTPS 证书的InsecureRequestWarning,如果不加处理,每次执行都会有告警刷屏。可以在文件顶部或者 conftest 里用warnings.filterwarnings处理,但比较优雅的方式就是在这个配置里加规则:ignore:.*InsecureRequestWarning.*:Warning。上面的模板里写的是通用规则,实际项目里你可以按需求增加。
4.2 结合 Allure 报告的功能组合
Allure 是接口自动化圈子里最常用的报告工具,pytest.ini里配合 Allure 的配置有几个关键点。
首先是addopts里的--alluredir=./report/allure_results,指定原始结果目录。这里有一个很多人忽略的细节:--alluredir指定的目录必须提前创建吗?答案是不用,pytest-allure 插件会自动创建目录。但如果目录里有上次执行的旧结果,新结果会叠加在里面,报告会显示重复数据。所以我在模板里加了--clean-alluredir,每次执行前自动清空旧结果。
其次是结合allure生命周期,建议在项目根目录下的conftest.py中定义钩子,动态设置报告的标题和环境信息。配置文件无法完全替代这些钩子,但pytest.ini的--alluredir是基础,没有它 Allure 数据根本不会生成。
注意,如果你同时使用了--alluredir和--html=xxx,两套报告会同时生成,互不冲突。有的团队喜欢 HTML 报告放到某个 Web 目录里共享,Allure 结果保留在本地,两套机制都能在pytest.ini里配置。不过我不建议把--html直接写进pytest.ini,因为 HTML 报告路径往往需要动态指定时间戳,更适合在 CI 命令里追加。贝壳项目里的做法是,本地用 pytest.ini 默认生成 Allure 结果,CI 里再专门生成 HTML 归档。
4.3 自定义配置项及其读取方式
pytest.ini不仅能配置 pytest 内置参数,还支持自定义配置项,供conftest.py或测试用例读取。这对接口自动化项目特别有用,因为 base_url、环境变量、超时时间、数据库连接串这类信息,放在配置文件里比硬编码在 Python 代码里方便得多。
自定义配置项的写法很简单,就是任意独一无二的键名:
[pytest] base_url = http://api.example.com test_env = dev timeout = 10然后在conftest.py里用pytestconfig.getini("base_url")获取:
import pytest @pytest.fixture(scope="session") def base_url(pytestconfig): return pytestconfig.getini("base_url")有一个坑需要提醒:如果自定义配置项跟 pytest 内置配置项重名,比如你写addopts = xxx这是正常覆盖;但如果写testpaths = xxx又写base_url = xxx,注意base_url虽然不是 pytest 内置项,pytest 会允许它存在,但某些旧版本会对无法识别的键名给出 warning,所以自定义键名一定要避免和内置关键字重名。
读取自定义配置还有另一个方式,是通过pytestconfig对象传入 fixture,这在接口自动化场景里经常用于新建 session 级别的客户端。比如所有用例共享同一个 requests.Session,base_url 从配置文件读取,环境变量从系统变量获取,两者在conftest.py中组合。这样做的好处是,切换测试环境时只需要改pytest.ini的test_env,不用动任何用例代码,非常契合多环境接口测试的需求。
5. 常见问题与排查技巧实录
配置pytest.ini的过程并不是一帆风顺的,这里记录几个我实际项目和社区里高频出现的问题,并给出排查思路,希望对你有实际参考价值。
5.1 中文注释乱码与文件编码问题
pytest.ini默认使用 UTF-8 编码解析。如果你用记事本在 Windows 上创建文件,默认可能是 GBK 编码,文件里有中文注释,pytest 读取时就会出现UnicodeDecodeError或乱码。解决办法很简单:用 VS Code、Sublime、Notepad++ 这类编辑器,保存时显式选择 UTF-8 编码,不要用系统记事本默认编码。
还有一种情况是文件保存为 UTF-8 with BOM 格式。pytest 的解析器对开头的 BOM 忍耐有限,某些版本会报parse error。如果你在文件开头看到“看不见的字符”,用十六进制查看器能发现EF BB BF,这就是 BOM。直接另存为无 BOM 的 UTF-8,问题就解决了。
5.2 配置文件不生效
如果你修改了pytest.ini但运行 pytest 时发现行为没有任何变化,先检查三个地方。第一,文件是否在 pytest 的 rootdir 下。pytest 启动时,会从当前目录向上查找配置文件,找到就停止,停止位置就是 rootdir。如果你在项目子目录里执行 pytest,而pytest.ini在根目录,那它不一定能被识别到,除非根目录路径在rootdir查找链上。最简单的排查方式是在项目根目录执行pytest --help,或者执行pytest --version查看输出中是否有rootdir这一行,路径和pytest.ini所在位置不一致,就说明文件没被加载。
第二,文件是不是被命令行参数覆盖了。举个例子,配置文件里写了markers,但命令行用了-o markers="xxx",那么-o的优先级更高,会覆盖配置文件的值。这类问题在集成 CI 时最容易发生,CI 脚本里拼参数拼多了,配置就被覆盖了。
第三,文件名拼写错误。这个在前面提过,pytest.ini一个字母都不能错。检查方法同上,先看rootdir输出了哪一行。
5.3 用例收集不全或收集了预期之外的用例
接口测试项目经常遇到两种情况。一是某个目录新增了用例,但在 pytest 运行时并没有被收集到;二是明明指定了 testpaths,却发现其它目录的用例也在运行。第一种情况多半是因为testpaths路径写错了,比如多了一个斜杠,或者大小写不敏感的系统上写的路径没有对齐。第二种情况往往出现在pytest.ini实际没被加载时,因为没加载,pytest 就按默认规则从当前目录递归收集,造成你感觉 testpaths 失效了。
用pytest --collect-only这个命令可以快速看到实际收集了哪些用例,这是排查这类问题最直接的手段。我强烈建议你改完 config 文件后跑一次pytest --collect-only -q,确认收集结果符合预期再跑完整用例。
5.4 ad hoc 过滤器与 addopts 互相干扰
一个比较隐晦的坑是,在addopts里配置了-m smoke,命令行再执行pytest testcases -m regression时,命令行参数会覆盖配置文件里的-m,这是正常行为。但如果有人在命令行执行的是pytest -m "smoke or regression",那么和addopts里的-m smoke之间,pytest 会使用最后出现的参数,导致结果和预期完全不一样。多人协作时,我建议在项目文档里明确说明:不要用命令行覆盖-m参数,所有用例筛选统一在pytest.ini中调整,或者统一约定在 CI 里追加覆盖。
5.5 关于自定义配置项的类型问题
pytestconfig.getini("timeout")返回的类型永远是字符串,这一点很多人会疏忽。如果你在pytest.ini里写了timeout = 10,拿到的就是字符串"10",而不是整数10。做超时计算时,直接用"10" + 1就会报 TypeError。解决办法是在 conftest 中显式转换类型:
import pytest @pytest.fixture(scope="session") def timeout(pytestconfig): return int(pytestconfig.getini("timeout"))这个细节在接口自动化里很关键,因为请求超时、重试次数、并发数都是数值型配置,一旦类型错了,表现就是有些环境跑得好好的,换个配置就报错。自定义配置项再多一点时,还可以写一个统一的配置解析函数,在 conftest 中一次性把所有参数转成 Python 数据类型。
6. 我踩过几次坑之后的个人总结
最后分享一点实际使用的心得。pytest.ini不是写一次就能永远不管的文件,它会随着项目规模、团队习惯、工具链的变化而演进。比如刚开始只有 smoke 和 regression 两种标记,后面加了 p1/p2/p3,再后来又加了 deploy、skip_deploy,这些都需要同步维护markers和addopts。我自己的经验是,每次修改完配置都要跑一遍pytest --collect-only -q,确认收集结果和标记筛选没被破坏。
另外一个建议是,把pytest.ini的变更记录写进提交信息里,因为它潜移默化地影响着每一次测试执行的结果。我曾经有一次因为把--disable-warnings不小心删掉,导致 CI 日志被警告刷爆,真正确认问题的时间远多于改配置的时间。
还有个细节算是我个人偏好的推荐:尽量保持pytest.ini简洁,不要把所有功能都堆进去。能用默认值就不用显式配,比如log_level = INFO这种,如果你没特殊需求,直接沿用默认即可,配置越多越难排查。接口自动化框架的核心是提升效率,如果配置文件反而成了障碍,就本末倒置了。
如果你们团队刚开始用 pytest 做接口测试,我建议第一周就把pytest.ini定下来,后面所有用例编写、执行、报告都以它为准。前期多花半小时把路径、标记、默认参数理清楚,省下来的是后面每个成员、每次 CI 执行时反复踩坑的时间。配置文件的威力不是体现在它有多少功能,而是体现在它能不能成为整个团队默认的、不需要再解释的执行标准。