☰
pytest安装与配置文件实战:从用例收集到报错排查
2026/9/28 17:46:36 网站建设 项目流程

这标题看着简单,但“安装”和“文件配置”两件事放在一起,就已经暗示了真正的问题:很多人装完pytest,兴冲冲写了一个test_x.py,然后命令行一跑,要么提示No tests were collected,要么明明有断言错误却显示通过,要么PyCharm里右键根本没有pytest选项。这些东西十有八九不是安装的锅,而是配置文件没跟上。我见过不止一个小白在群里问“pytest是不是坏了”,最后发现只是pytest.ini里的testpaths指错了目录。

所以我这篇不打算只写“pip install pytest就完事了”,而是把从环境准备、核心配置文件、到排查链路的完整思路都过一遍。你要想清楚一个前提:pytest本身是个很轻的框架,它真正强大的是那套“约定大于配置”的收集规则,以及pytest.ini、conftest.py、pyproject.toml这几个配置载体。安装只解决“有没有这个工具”,配置才解决“它听谁的、怎么听”。

1. 安装前先想清楚:pytest靠什么找到你的测试用例

1.1 从unittest切换到pytest的真实理由

我在早期项目里用的也是unittest,后来切到pytest,最直观的感受不是功能多,而是“少写了很多样板代码”。unittest里你要写class TestXXX(unittest.TestCase),每个断言都要通过self.assertEqual这种形式来调用,而pytest直接允许你用Python原生的assert,失败了还会自动把表达式两侧的值都展开到报告里。

但这还不是最关键的差异。更本质的是收集机制。unittest靠测试套件和loader去加载,而pytest使用一套目录扫描规则,按文件名、类名、函数名自动收集用例。也就是说,只要你按它的约定命名,它就能找到你写的所有测试,不需要你去维护一个suite()函数。这个“约定大于配置”的设计,才是pytest能在工程里立足的核心原因。

另一个大杀器是fixture机制。unittest的setUp/tearDown是按类或方法级别来的,跨模块共享非常麻烦。pytest的fixture可以定义在conftest.py里,按作用域自动注入,还能像函数参数一样声明依赖关系。用惯了之后,你很难再回到setUp那套写法里去。

我用一张表给没接触过unittest的读者做一个快速对比:

对比维度pytestunittest
断言方式原生assert,失败信息自动展开assertEqual、assertTrue等方法
用例收集按文件名和函数名自动扫描需要TestCase类,用loader加载
前后置逻辑fixture,按需注入,支持多层依赖setUp/tearDown,按类或模块
插件生态非常丰富,官方和第三方集成成熟相对较少
配置方式pytest.ini / pyproject.toml / conftest.py多为代码内配置

1.2 三层命名规则决定了它能不能“看到”你的用例

pytest在收集用例时,默认遵循一套规则,如果你不了解,配置再漂亮也没用。

第一层是文件命名。模块名必须匹配test_*.py或者*_test.py。比如test_user.py没问题,但如果你把它命名为check_user.py,pytest默认就直接忽略掉。

第二层是类命名。以Test开头的类会被认定为测试类,注意是首字母大写T,且类名中不能有__init__构造函数,否则也会影响用例收集。类里面的方法如果以test_开头,会被识别为测试用例。

第三层是函数命名。模块里的顶层函数,只要以test_开头,就会被收集为用例。

这三层规则合在一起,就是pytest的默认“雷达扫描范围”。很多人刚装好pytest,建了一个名字完全不合规的目录和文件,然后抱怨“pytest一个用例都没跑”,其实不是它坏了,是它还没到你规划的雷达范围里。

1.3 安装和文件配置其实是两个正交问题

我见过不少人有这个误区:pip安装成功后,就觉得万事大吉了。其实安装只是第一步。你可以把安装理解成给系统里注册了一个可执行程序,而配置文件决定的是这个程序在具体项目里的工作目录、扫描范围、标记规则、插件加载行为。

pytest的配置载体有很多个,常用的是pytest.ini、conftest.py和pyproject.toml,它们的职责侧重点完全不同。pytest.ini管的是全局行为,比如收集规则、命令行默认参数、marker注册;conftest.py管的是测试代码的共享设施,比如fixture、钩子函数;pyproject.toml则是在项目用统一配置管理时,把pytest配置并入其中的一种方式。

所以你在动手之前,先把“安装”和“配置”这两件事在脑子里分开。后面排错时,思路会清晰得多:命令找不到是安装问题,收集不到用例是命名或配置问题,fixture报错是conftest问题。

2. 环境准备:虚拟环境、pip安装与解释器选择的实操细节

2.1 Python版本和虚拟环境先定下来

pytest目前对Python版本的要求比较友好,推荐使用3.8及以上版本。Python 2.7的年代已经彻底过去了,现在还在生产环境用Python 2跑pytest的项目,我建议尽早升级,因为新版本pytest早就放弃了对Python 2的支持。

虚拟环境这块,我强烈建议不要直接往系统Python里装pytest。原因很简单,系统Python往往被各种系统工具依赖,用pip直接装包,很容易造成依赖版本冲突。比如系统里某个工具需要旧版click,而pytest插件又要用新版,pip一升级就把系统环境搞崩了。

我自己的习惯是每个项目建一个虚拟环境。工具上用Python自带的venv就够了,如果你已经在用Miniconda或者Anaconda,那用conda创建环境也没问题。两者选哪个主要看你是否还需要管理不同Python版本。如果只是日常跑pytest,venv轻量省事;如果还需要多个Python版本切换,conda会更顺手。

创建虚拟环境的命令也简单:

# Windows 下,在项目根目录执行 py -m venv .venv # macOS / Linux 下执行 python3 -m venv .venv

创建完目录之后,还需要激活它。Windows上的激活脚本是.venv\Scripts\activate,macOS和Linux上要用source .venv/bin/activate。激活成功之后,命令行提示符前面会出现(.venv),到这才算真正进入隔离环境。

2.2 三步完成pytest安装

激活虚拟环境之后,安装pytest其实就一条命令:

python -m pip install pytest

这里有个细节我要特意说一下:我习惯用python -m pip而不是直接用pip install。理由是,在Windows多Python环境或者macOS下,直接输pip指向的可能是另一个解释器对应的pip,而你当前shell里用的Python可能完全不是同一个。用python -m pip可以确保包安装到当前这个python解释器里,从源头规避“装到了A环境,B环境找不到”的经典问题。

国内网络环境不好的情况下,可以临时指定镜像源安装:

python -m pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple

镜像源用法我就不多展开了,更推荐的做法是安装完后自己验证一下。验证方式有两个,一个看版本,一个看过解释器路径:

# 查看版本 pytest --version # 查看可执行文件路径,确认它在你的虚拟环境里 which pytest

在Windows上如果pytest --version提示命令找不到,但是python -m pytest --version能正常输出,那说明pytest包本身装好了,只是Scripts目录不在PATH里。最简单粗暴的办法就是用python -m pytest跑命令,而不是去改系统PATH环境变量。这也解释了为什么我后面所有示例都会优先用python -m pytest,不是打起来麻烦,而是没有路径依赖,换到哪个环境都能跑。

2.3 装完先在IDE里做两件小事

很多人喜欢在PyCharm或VSCode里直接跑测试,但如果IDE没有正确指向虚拟环境里的解释器,就会出现“终端里pytest好好的,IDE里全是红叉”的诡异现象。

PyCharm里需要到File -> Settings -> Project -> Python Interpreter,点右上角的齿轮,选择Add Local Interpreter,把虚拟环境目录下的python.exe或bin/python加进去。紧接着还要去Settings -> Tools -> Python Integrated Tools,把Default test runner设为pytest。这一步经常被忽略,默认还停在unittest上,右键运行的时候自然就是一堆unittest的报错。

VSCode里则需要安装Python扩展,然后通过Ctrl+Shift+P打开命令面板,执行Python: Select Interpreter,选择虚拟环境那个解释器。同时要让测试框架识别Pytest,一个比较直接的方法是命令面板里执行Python: Configure Tests,然后选择pytest作为测试框架。这样测试面板里才能正常发现用例。

这些操作不属于pytest包本身的内容,但如果你跳过了,后面会有非常多莫名其妙的“跑不起来”。我把它们放在安装部分一起讲,是为了让你在进入文件配置之前,先把“哪个解释器”这个问题彻底锁死。

3. 文件配置:pytest.ini、conftest.py、pyproject.toml的分工与写法

3.1 pytest.ini应该是你的首选项

pytest支持的配置文件有好几个,但实战里我最推荐的是pytest.ini。原因是它简单直接,pytest版本对它的兼容性最好,不像pyproject.toml里的[tool.pytest.ini_options]有时会因为插件版本太旧而出现识别不全的问题。

所谓“全局行为”,指的是它影响的是pytest整个运行阶段,而不是某个测试文件。下面这个例子几乎是我每个项目的标准开头:

[pytest] minversion = 7.0 testpaths = tests python_files = test_*.py *_test.py python_classes = Test* python_functions = test_* addopts = -ra -q --strict-markers markers = smoke: 快速验证主链路 slow: 长时间运行的用例 filterwarnings = ignore::DeprecationWarning

一个字段一个字段说。

minversion是告诉pytest,如果当前环境的版本低于7.0就直接报错,避免配置文件里用到新语法但实际跑在旧版上。testpaths指定了运行整个项目时要扫描的目录,这里指定的是tests。这意味着如果我在项目根目录下直接敲pytest,它只会去tests目录找用例,而不是把整个仓库扫一遍。这个设置对大型项目意义极大,能显著减少启动时的文件扫描时间。

python_files、python_classes、python_functions是对第一节说的默认命名规则的显式声明。不写,pytest也用这套默认值;写了,你随时可以改。如果你希望某些特殊命名的文件也能被收集,改这里就行。

addopts是命令行默认参数,我这里写了-ra -q --strict-markers。-ra让pytest在结束的时候汇总输出所有被跳过、被xfail、失败的用例;-q是精简模式;--strict-markers是强制要求所有marker都必须在配置文件里注册过,这个选项看起来不起眼,实际作用很大。它可以拦截一个常见的坑:你把@pytest.mark.smoke写成了@pytest.mark.smke,如果没有--strict-markers,pytest只会给一个warning,然后静默跳过,你花两小时也查不出为什么这批用例没跑。开了之后,它直接报错,你一眼就能看到。

markers负责登记项目中会用到的所有标记。标记不仅在逻辑上起分组作用,它还能配合-m参数做筛选用例。比如-m smoke只跑冒烟用例。注册marker的意义在于,团队里每个成员写到这个标记都不会出现莫名其妙的拼写问题。

filterwarnings是过滤告警的配置项。实际项目里依赖库往往会输出大量DeprecationWarning,如果一个个处理很耗费精力,可以先按下不表,把主流程跑通再逐步清理。这里ignore::DeprecationWarning的意思就是忽略所有弃用告警。

3.2 conftest.py:测试环境的“地基”

如果说pytest.ini是全局规则的中枢,那么conftest.py就是测试代码的共享层。它可以定义fixture、hooks、加载外部插件,并且以它所在目录为界,对当前目录及其子目录里的所有测试文件生效。

它最常用的功能是定义跨模块共享的fixture。举个例子,假设你的测试里很多用例都要访问一个API基地址,你当然可以在每个test文件里重复定义一个fixture,但那样既有重复代码,又不容易统一维护。更合理的姿势是放在conftest.py里:

import pytest @pytest.fixture def api_base_url(): return "http://127.0.0.1:5000" @pytest.fixture def auth_token(): return "token-demo-123456" @pytest.fixture(autouse=True) def _print_case_header(): print("\n===== 开始执行用例 =====")

第一个fixtureapi_base_url是显式依赖的,只有用例参数里写了api_base_url,它才会被注入。第二个同样是显式的,用来模拟登录态。第三个不一样,它带了一个autouse=True参数,也就是说当前目录下的所有测试用例在执行前都会自动调用它,不需要在用例参数里显式声明。

这里有个细节容易被误解:conftest.py在哪个目录,fixture就对哪个目录及其子目录生效。如果项目根目录下有一个conftest.py,而tests目录下又有另一个conftest.py,这两个文件定义的fixture是分层的,内层conftest里定义的名称,只在内层目录范围可见。同名fixture的情况下,内层会覆盖外层,这个覆盖逻辑在排错时很有用。

3.3 想统一用pyproject.toml时怎么办

有些项目为了保证Python包管理的统一,习惯把构建配置、依赖声明都放在pyproject.toml里,不想再单独维护一个pytest.ini。这种场景确实没问题,pytest支持在pyproject.toml里通过[tool.pytest.ini_options]段完成同样的配置。

[tool.pytest.ini_options] minversion = "7.0" testpaths = ["tests"] addopts = "-ra -q --strict-markers" markers = [ "smoke: 快速验证主链路", "slow: 长时间运行的用例", ] filterwarnings = ["ignore::DeprecationWarning"]

写法整体差异不大,唯一要注意的是数组类型的字段要写成一行的列表形式,比如testpaths和markers。如果你把testpaths写成字符串"tests",有些pytest版本会以字符串作为单一目录名处理,容易出现路径拼接错误。稳妥起见,直接写成列表,后面加不加逗号都行。

这里需要强调一个优先级问题:如果项目里同时存在pytest.ini和pyproject.toml,pytest会优先加载pytest.ini,此时pyproject.toml里的pytest配置会被忽略。所以你在一个项目里看到一堆配置文件时,先判断谁在生效,不然改半天pyproject.toml发现根本不生效,那才是真浪费时间。

3.4 rootdir与配置加载优先级

rootdir这个概念,很多人用了很久pytest也没真正理解,但它恰恰是文件配置不生效的重灾区。pytest在启动时会从当前执行目录向上查找第一个包含pytest.ini、pyproject.toml、tox.ini或setup.cfg的目录,把它确定为rootdir。你可以通过运行pytest时命令行第一行的输出看到它的值。

rootdir决定了相对路径是怎么解析的。比如配置文件里写了testpaths = tests,这个tests是相对于rootdir来定位的。如果你在项目根目录执行pytest,一切正常;但如果你站在某个子目录里执行pytest,它会向上找rootdir,然后用rootdir下的tests路径去扫描,这也解释了为什么很多人换了一个目录执行pytest后,用例反而找不到了。

如果你确实需要手动指定rootdir,可以在运行命令里加--rootdir参数。但我在项目实践中更推荐把配置文件放在项目根目录,并且固定从根目录执行pytest。这样rootdir永远在预期位置,不会因为终端cd到了不同目录而出现诡异行为。

最后把配置载入顺序也明确一下,方便你判断手头项目到底哪个文件在起作用:pytest.ini的优先级最高,其次是pyproject.toml,然后是tox.ini,最后是setup.cfg。

4. 用一套真实小项目验证每个配置项

4.1 标准目录与用例写法

聊了这么多配置理论,是时候用一个最小项目验证它们真的生效。我搭建一个实际的目录结构:

project/ ├── pytest.ini ├── conftest.py └── tests/ ├── test_user.py └── test_order.py

用例文件内容尽量精简,突出各种收集规则:

# tests/test_user.py def add(a, b): return a + b def test_add(): assert add(2, 3) == 5 class TestCalculator: def test_sub(self): assert 5 - 3 == 2

这个时候配置文件里的python_files = test_*.py *_test.py已经在生效了,test_user.py符合规则,里面的test_add函数和TestCalculator类方法都会被收集。运行:

python -m pytest -v

输出大致是:

tests/test_user.py::test_add PASSED tests/test_user.py::TestCalculator::test_sub PASSED

如果你能看到这个输出,说明环境和最基础的收集规则都没有问题,接下来就能“折腾”配置了。

4.2 逐条验证配置效果

第一个试验,验证testpaths。我把pytest.ini里的testpaths保留为tests,然后在项目根目录新建一个demo_test.py,里面写一个test_demo()函数。运行pytest,你会发现demo_test.py根本不会被收集。这就是testpaths在起作用,它把所有扫描范围限定在了tests目录。你如果不想要这个限制,把该项删掉,或者显式改成testpaths = tests demo_test.py,它才会进入扫描范围。

第二个试验,验证python_files。把tests/test_order.py改名为tests/orders.py,函数名不变。然后运行:

python -m pytest --collect-only -q

注意看收集结果,test_order.py里的用例全部消失,因为orders.py不再匹配test_*.py或*_test.py。这个试验能帮你理解,为什么有些人写的测试文件明明内容完全正确,pytest却不执行。

第三个试验,验证addopts。配置里写了--strict-markers,那我在用例里加一个未注册的marker:

import pytest @pytest.mark.notregistered def test_demo(): assert True

运行pytest时会直接抛错,提示marker没有注册。这个行为能热到“造的每个marker都需要在配置文件里留名”的习惯。

第四个试验,验证marker筛选用例。在test_user.py里加两个marker:

import pytest @pytest.mark.smoke def test_add(): assert add(2, 3) == 5

运行python -m pytest -m smoke,你会看到这次只执行了标记为smoke的用例。测试项目越杂,-m参数就越有用,比如“只跑慢用例”“只跑数据库相关用例”。

4.3 命令行和IDE跑出不同结果时怎么定位

实际开发里我经常遇到一种情况:在命令行里一切正常,但IDE里的测试面板就是红彤彤一片。排查步骤我一般按这个顺序来。

第一,确认IDE的解释器是不是虚拟环境里的那个。PyCharm看右下角或Setting里的Interpreter,VSCode用命令面板执行Python: Select Interpreter检查。很多时候问题就出在IDE默认用了一个系统级别的解释器,这个解释器里根本没有pytest。

第二,确认IDE的默认测试运行器。PyCharm里如果还停留在unittest,它发现不了pytest的用例;VSCode里则需要启用python.testing.pytestEnabled配置项,或者执行Python: Configure Tests重新选择框架。这个过程不需要重装pytest,是纯配置问题。

第三,确认工作目录。IDE运行测试时的工作目录可能被设置为某个项目子目录,而不是项目根目录。这种情况下,如果配置文件在根目录,通过testpaths相对定位可能会失效。在PyCharm的Run Configuration里手动设置Working Directory为项目根目录,通常能解决。

5. 安装配置后的高频翻车点:完整排查链路与解决实践

5.1 pytest命令找不到/版本不对

这是刚装完pytest之后遇到的最多的问题。现象就是控制台里输入pytest --version,系统提示找不到命令。

先不用慌,试着跑一下python -m pytest --version。如果这条命令正常输出版本号,说明pytest已经安装到了当前Python解释器,只是Scripts目录没有加入PATH环境变量。对多数项目来说,直接用python -m pytest代替pytest命令就能正常使用,没必要为了这个去改系统PATH。

如果python -m pytest也提示找不到模块,那就检查一下你当前Python是不是虚拟环境里的那个。可以用python -c "import sys; print(sys.executable)",看看打印出来的路径是否指向.venv目录。如果指向的是系统Python,说明激活虚拟环境那一步没生效,重新检查激活命令,Windows PowerShell下还要注意执行策略可能拦截activate脚本。临时放开当前进程执行策略的话,用下面的命令:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

5.2 No tests were collected

这个提示几乎是每个pytest用户的必经之路。看到它不要怀疑人生,按顺序查四件事。

第一,文件名是否符合规则。是不是叫test_x.py或者x_test.py;如果叫xxx.py、check_yyy.py,默认就不会被收集。第二,函数名或类名是否符合规则,函数得是test_开头,类得是Test开头。第三,配置文件里的testpaths指向的目录是否存在,如果目录路径拼错,pytest一进来扫描就找不着北。第四,跑到pytest --collect-only -q,让它把你当前的收集结果打出来,一目了然。

还有一个容易被忽略的点是文件编码。如果测试文件是GBK编码,而文件开头又没有声明编码注释,Python解析时可能出现UnicodeDecodeError,进而导致该文件整个不被收集。统一把源码文件存成UTF-8是根治方式。

5.3 fixture找不到

fixture报错长这样:fixture 'xxx' not found。排查链路相对清晰。

第一步,确认fixture定义在哪个conftest.py里,以及当前用例是否在该conftest的目录范围内。conftest的可见性是向下的,如果用例在tests/sub/下面,而fixture定义在tests/sub/conftest.py里,那么tests/下其他模块是看不到它的;把它挪到tests/conftest.py里,范围内所有子目录才都能用。

第二步,确认没有同名覆盖。子目录conftest里的同名fixture会覆盖外层conftest里的定义,如果你发现fixture行为诡异,看看是不是存在这种覆盖。

第三步,可以直接运行pytest --fixtures,它会列出当前环境中所有可见的fixture列表,以及它们定义的文件位置。当你手工排查半天都没结果时,这个命令可以帮你快速链条化定位。

5.4 编码、中文路径与控制台输出乱码

Windows环境下的坑,很多是编码带来的。比如用例文件里有中文字符串,控制台输出时可能出现乱码,或者出现类似UnicodeEncodeError: 'gbk' codec can't encode character的报错。

这一类问题不属于pytest本身的bug,而是Python在Windows控制台下的stdout编码用了系统的GBK编码,没法输出某些字符。一个很实用的处理方式是在运行测试之前设置环境变量:

set PYTHONUTF8=1

macOS和Linux下则是:

export PYTHONUTF8=1

设置之后,Python会强制使用UTF-8模式处理IO。你也可以考虑在测试文件的头部加# -*- coding: utf-8 -*-,但在Python 3时代这更多是一个声明习惯,真正解决输出乱码还是靠环境变量或设置PYTHONIOENCODING=utf-8。

5.5 插件冲突与配置覆盖

pytest生态的插件是一把双刃剑。装了一堆插件之后,偶尔会出现一些诡异问题,比如用例执行顺序被打乱、报告生成不了、cover参数无效。我遇到这类情况,第一步先看pytest --version,注意它底部会列出所有已加载的插件清单,这个输出本身就是很好的排查入口。

怀疑某个插件的时候,可以用-p no:插件名临时禁用它。例如:

python -m pytest -p no:randomly

这个参数的意思是“不加载randomly插件”,如果禁掉之后问题消失,那就是插件冲突无疑了。

还有一种情况是配置文件的覆盖问题。如果项目里同时存在pytest.ini和pyproject.toml,pytest默认使用前者,后者里的[tool.pytest.ini_options]完全不生效。你要是发现改pyproject.toml里的配置没有反应,请先检查根目录下是不是还有一个pytest.ini在“截胡”。

5.6 大型套件运行时的资源问题

一般写测试写到几百上千个用例时,还会遇到一类环境层面的问题:测试进程被系统杀掉,或者整个机器变得卡顿。如果你在Windows上运行大型测试集时看到系统提示“由于启动计算机时出现了页面文件配置问题,Windows在你的计算机上创建了一个临时页”,同时pytest进程异常退出,这基本说明机器内存或虚拟内存已经临界了。

这种情况下不是pytest配置能解决的,重点在于控制并发和分片。可以先用-n auto看看是不是多进程并行导致的内存尖峰,如果杀掉进程的现象还是存在,就老老实实减少并发或改用-k按模块分片跑,比如:

python -m pytest tests/test_user.py tests/test_order.py -x

还有一个小技巧是给addopts里加--maxfail=3,防止失败用例数量一多就连续跑一大堆,对资源也是一种保护。

5.7 一张表快速定位常见问题

把前面提到的现象、排查重点和快速操作整理成一个表格,方便你以后遇到问题时直接对号入座:

现象优先检查快速处理
pytest命令找不到虚拟环境是否激活,Scripts/bin目录是否在PATH改用python -m pytest
No tests were collected文件命名、函数命名、testpaths路径运行pytest --collect-only -q
fixture not foundconftest所在目录、名称拼写、同名覆盖运行pytest --fixtures
控制台报编码错、乱码文件编码、Windows控制台编码设置PYTHONUTF8=1
插件导致行为异常插件清单、插件冲突用-p no:插件名禁用测试
大型测试进程被杀内存和并发资源减小并发、分片执行、--maxfail
配置改了半天不生效多个配置文件并存、rootdir指向确认pytest.ini优先级,打印rootdir

最后说一个我自己养成的习惯吧,也是给所有刚用pytest的人的建议。新环境里装完pytest,不要急着写一大堆用例,先跑两条命令探路:一条是pytest --collect-only -q,确认pytest到底能看到哪些用例;一条是pytest --fixtures,确认当前环境里有哪些fixture可见。这两个探路命令能让你在动手之前,先对“pytest当前视角里的项目长什么样”有个底。在这个基础上再去动pytest.ini、conftest.py,你改每个配置项时都能立刻看到它在真实运行中的反馈,效率比盲改高得多。

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

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

立即咨询