ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Anaconda 环境下 pytest 测试运行器配置与排查指南

Anaconda 环境下 pytest 测试运行器配置与排查指南

直接在 Anaconda 里跑 Python 单元测试,听起来就像是“装个 pytest 然后点运行”这么简单。但真正上手之后你会发现一堆问号:conda 环境明明激活了,测试运行器却还是找不到解释器;pytest 在终端里跑得好好的,换到代码编辑器里就变成 0 个用例;更崩溃的是,昨天还能跑通的测试,今天因为某个依赖版本变化,直接给你红一片。我过去在 Anaconda 上折腾测试运行器的经验,基本就是把这些坑一个个踩平的过程。这篇文章围绕“Anaconda 测试运行器”这个主题,把配置思路、实操步骤、排查经验一次讲清楚。

它到底解决什么问题?简单说,就是让“写测试”“跑测试”“看结果”这三件事在一个统一、干净、可复现的环境里跑通。适合三类人:刚接触 pytest 但被环境配置劝退的新手;还在拿 base 环境直接跑项目测试、哪天被依赖搞崩的老哥;以及想把测试流程固化到团队协作里的开发者。

1. 先把家底理清:Anaconda与测试运行器的适配思路

1.1 为什么你总觉得“Anaconda里跑测试”很别扭

很多人在 Anaconda 里跑测试,别扭的根源并不是不会写测试代码,而是没搞清楚 Anaconda 到底在整个测试流程里扮演什么角色。Anaconda 是一个 Python 发行版,它的价值主要包括两个部分:conda 环境和 conda 包管理工具。conda 环境是一套可切换的 Python 解释器加依赖集合,conda 包管理则负责维护这个集合里每个包的版本。单元测试运行器则是另一个东西,它是一个读取测试文件、执行测试、采集结果的机制,可能是 IDE 里的按钮,也可能是 pytest 这样的框架自带命令。

两者之间的关系很像厨房操作台和厨师:conda 环境是操作台,提供切菜、盛菜所需的空间和工具;测试运行器是帮你端菜的人,他只认你指定给他的那一台操作台。如果你没告诉运行器“这台操作台在哪一个路径”,它默认就会跑去 base 环境找工具。base 就是 Anaconda 自带的那个默认环境,什么包都有,但正因为什么都有,它才是最容易失控的地方。某天你在 base 里 pip install 了一个包,版本覆盖了项目需要的内容,测试结果就会变得不可复现。

1.2 一套好用的测试运行器由什么组成

把测试运行器拆开看,其实只有三层。第一层是环境层,核心是 conda 环境的解释器路径,以及在这个环境里装好的 pytest、pytest-cov 这类测试框架包。第二层是框架层,也就是 pytest 的配置,包括测试目录在哪里、用什么文件命名规则、要不要禁用某些 warning。第三层是运行层,可以是命令行里的 pytest 命令,也可以是现代代码编辑器的测试插件,负责把测试发现结果展示给你。

这三层只要有一层对不上,就会出问题。最常见的场景是:环境层你创建了一个干净的 envs/projectX,框架层也在里面装好了 pytest,结果运行层却指向 base 的 python,于是运行器用的还是 base 里的 pytest。更隐蔽的情况是命令行跑的是 projectX 里的 pytest,正确;但代码编辑器的测试插件默认选了解释器 base 的 python,于是两边结果不一致。理解了这三层,后面所有配置都会变得有迹可循。

2. 五分钟搭好基础战场:conda环境与测试框架安装

2.1 创建专用conda环境,别在base里裸奔

我见过很多开发者,打开 Anaconda Prompt 直接就开始写测试,跑的时候也在 base 环境里跑。短时间确实没事,但某一天项目装上新依赖,或者想要换 Python 小版本,你就会发现测试结果开始漂移。正确做法是为每个项目创建一个独立的 conda 环境。命令很简单:

conda create -n projectX python=3.10 -y conda activate projectX

这里的 projectX 可以换成你的项目代号,python 版本建议选一个你项目运行的版本,不用刻意追最新,稳定性优先。激活之后,重点确认当前环境的解释器路径:

which python

在 Windows 上对应是where python。这一步非常关键,后面配置测试运行器的时候,要找的就是这个路径。如果你创建了环境但 activate 不生效,检查一下是不是在非交互的脚本里漏了激活步骤,或者用的是系统自带的 Python 配置,而不是 Anaconda 的 conda 初始化配置。

注意:尽量不要把项目测试跑在 base 环境里。base 环境被污染以后,恢复成本很高,甚至只能重建 Anaconda。而独立的 conda 环境出了问题,删除重建也就几秒钟的事,这是环境隔离最大的价值。

2.2 把测试依赖固化到requirements-dev.txt

环境隔离做得好,还要依赖版本稳定。很多项目只在 README 里写“pip install pytest”,这等于没写。正确的做法是用 requirements-dev.txt 把测试相关依赖固定下来:

pytest>=7.4,<8.0 pytest-cov>=4.1,<5.0

划重点:这里的版本约束不是越精确越好。这种写法,允许你在小版本内自动更新,又不会因为大版本变更导致配置不兼容。安装的时候直接:

conda run -n projectX pip install -r requirements-dev.txt

这里用conda run -n projectX而不是先 activate 再 pip install,在脚本里也能自动切到对应环境,减少人为遗漏。如果你是团队协作场景,最好再把运行环境依赖也放进另一个 requirements.txt,这样其他人拿到项目,一条命令就能把所有依赖装齐。

2.3 测试框架选型:pytest为主,unittest做备选

Anaconda 自带 unittest,很多人就直接用 unittest 写了。我没有反对 unittest,但如果你要在“测试运行器”的体验上少踩坑,我还是建议用 pytest。给你一张对比表:

对比点pytestunittest
断言写法assert 表达式,直观必须用 assertEqual、assertTrue 等方法
临时文件与资源清理fixture 和 tmp_path 很顺滑需要 setUp/tearDown 手写管理
参数化测试@pytest.mark.parametrize 一行搞定需要 subTest 或多次复制
插件生态覆盖率、超时、顺序控制等插件丰富相对贫乏

pytest 还有一个很大的优势是“测试发现”规则简单:默认匹配 test_ 开头的文件里 test_ 开头的函数。unittest 则需要你记住继承 TestCase、方法名要 test 开头等一堆约束。选择 unittest 的唯一理由,大概是某些老项目已经用它写了一大半,改造成本太高。如果是新技术选型,或者只在 Anaconda 环境里跑自己的小型项目,直接上 pytest 更省心。

3. 配置测试运行器:从命令行到图形界面

3.1 先让命令行成为唯一真理:pytest.ini配置详解

我最开始配置测试运行器,是在代码编辑器里折腾各种图形选项,后来发现图形界面能给你的配置项,命令行几乎都能覆盖,而且命令行配置更容易通过配置文件沉淀下来。所以建议你在项目根目录创建一个 pytest.ini 文件,写上:

[pytest] minversion = 7.0 testpaths = tests python_files = test_*.py *_test.py python_functions = test_* addopts = -ra -q --strict-markers filterwarnings = error

一个个解释。minversion 用来防止老版本 pytest 跑出诡异行为。testpaths 告诉运行器去 tests 目录下找用例,搜索范围越小,发现速度越快。python_files 和 python_functions 定义了 pytest 的惯用规则“test_ 开头”,这里我再加了一个 *_test.py,兼容部分团队的命名习惯。addopts 里 -ra 会在最后汇总所有失败、跳过原因;-q 安静模式;--strict-markers 强制标记必须注册,防止你随手打个不存在的 mark 然后困惑半天。filterwarnings = error 是我特别推荐的一项,它把 warning 直接变成测试失败,这样 deprecation 警告会提前暴露,而不是悄悄埋在日志里。

写完后,在终端里验证一下:

conda run -n projectX pytest

如果这时候所有用例通过,那说明框架层和环境层是通的。这里记住一个原则:命令行跑不通的,图形界面也别指望能跑通。因为所有运行器底层最终都是调 pytest 命令。

3.2 在代码编辑器里接上同一套pytest

命令行跑通之后,再配代码编辑器就是顺理成章的事。以我常用的编辑器为例,它会先通过“选择解释器”功能来定位 Python 环境。你需要在这里选择之前创建的那个 conda 环境,而不是 base。具体路径一般能直接搜到,也可以在编辑器设置里手动填成类似这样的位置:

/path/to/anaconda/envs/projectX/bin/python

Windows 上则是C:\Users\...\Anaconda3\envs\projectX\python.exe。选完解释器,再安装对应语言的测试插件,在测试配置面板里把测试框架设为 pytest,pytest 的配置文件路径指到项目根目录。到这里,编辑器会扫描你项目下的测试文件,左侧会出现一个测试列表,每个用例旁边有运行的按钮。如果你选错了解释器,最常见的结果就是运行测试时提示找不到 pytest,或者 import 项目模块时报 ModuleNotFoundError。

3.3 另一款常见IDE的配置快速参考

如果你用的是另一款流行 IDE,配置逻辑也一样,只是入口不同。通常在运行配置或 Python 解释器设置里,可以添加 conda 环境,选择之后为 pytest 创建一个运行配置,把工作目录和 pytest 参数填进去。关键点仍然是解释器路径和配置文件,这两个对了,其他都是锦上添花。我建议团队协作时,把 pytest.ini 的路径约定写进项目文档,这样不管大家用哪款编辑器,配置目标都是一致的。

3.4 配置界面背后的逻辑:解释器、根目录、匹配规则

很多人被代码编辑器的测试设置界面搞晕,是因为没理解界面上的控件都对应什么。仔细看,本质只有三个变量:一是解释器路径,决定用哪套环境;二是测试根目录,对应 pytest.ini 里的 testpaths;三是文件匹配规则,对应 python_files。图形界面那堆选项,大多只是把这三个变量换了个说法。你只要在命令行里把 pytest 跑通,再对照这三项去填图形界面,基本不会出错。

4. 写出能被运行器识别的测试代码:从用例到工程化

4.1 最基础的可发现用例

测试运行器的第一步是“发现测试”。pytest 的发现机制听起来简单,但很多人一开始就栽在这里。看一个最小用例:

def test_addition(): assert 1 + 1 == 2

把这个函数放在一个名为 test_demo.py 的文件里,pytest 就能发现它。规则有三个:文件名必须以 test_ 开头或test 结尾;文件内容里的函数名以 test开头;函数体里用 Python 原生的 assert 做断言。别小看这三条,如果你把文件命名为 demo.py,或者函数名写成 check_addition,就算代码逻辑完全正确,运行器也会找不到它。还有一种容易踩的坑是把文件取名 test.py,这样会和标准库里的测试工具别名冲突,某些场景下行为会很怪,我吃过这个亏,建议避开这种命名。

4.2 用fixture解耦依赖:实操示例

当你打算测试的东西带有外部资源(数据库连接、临时配置、文件读写)时,fixture 就派上用场了。fixture 可以理解成一个自动执行“准备加扫尾”的钩子。比如你有一个数据库连接类,想测试它的连接状态:

import pytest class Database: def __init__(self): self.connected = False def connect(self): self.connected = True def close(self): self.connected = False @pytest.fixture def db(): db = Database() db.connect() yield db db.close() def test_connection(db): assert db.connected is True

这里 yield 之前的代码在测试前执行,yield 之后的代码无论测试是否失败都会执行,相当于 finally 清理。测试函数把 db 当作参数传入,运行器会自动把 fixture 的返回值送进来。这样做的好处是:测试用例本身不需要关心连接和关闭资源的细节,也不用担心数据泄露到其他测试里。如果多个测试需要用同一个成本高昂的 fixture,可以把 fixture 的作用域改成@pytest.fixture(scope="module"),让整个模块只准备清理一次。作用域是新手容易忽略的调优点,提醒一下。

4.3 覆盖真实场景的三件套:参数化、异常断言、临时文件

pytest 能流行,很大程度靠下面三个特性。第一个是参数化,可以一份断言逻辑吃下多组数据:

import pytest @pytest.mark.parametrize("num, expected", [(2, 4), (3, 9), (10, 100)]) def test_square(num, expected): assert num * num == expected

运行时你会看到这其实是 3 个用例,任何一个数据挂了都能单独定位。第二个是异常断言,用来验证“当输入不合法时,代码会抛出正确异常”:

def test_invalid_input_raises(): with pytest.raises(ValueError): int("abc")

这里必须用 pytest.raises,不能只写 try except,因为后者如果没进异常分支,测试仍然通过,等于什么都没测。第三个是临时文件处理,pytest 提供了 tmp_path fixture:

def test_write_log(tmp_path): log_file = tmp_path / "log.txt" log_file.write_text("hello") assert log_file.read_text() == "hello"

tmp_path 会给每个测试生成独立临时目录,测试结束自动清理,不会污染项目目录。这三个特性组合起来,日常开发里 80% 的测试场景都能覆盖。

4.4 工程化:conftest.py与覆盖率

单文件写测试简单,项目一大就要讲工程化。第一个工程化工具是 conftest.py,放在 tests 目录下,它定义的 fixture 可以被同目录及子目录下所有测试文件共享,不用每个文件重复引入。第二个工具是覆盖率。Anaconda 环境里安装过 pytest-cov 后,命令行这样跑:

conda run -n projectX pytest --cov=src --cov-report=html

--cov=src 表示统计 src 源码目录的覆盖率,--cov-report=html 会生成 htmlcov 目录,打开就能看到哪些行没被执行。覆盖率并不是越高越好,但可以辅助你发现完全没测到的分支。一个比较合理的起步目标是新代码的行覆盖在 80% 以上,而不是一股脑把所有历史代码都往上拉。

5. 实战排查:Anaconda加测试运行器最常翻车的场景

5.1 最常见的ModuleNotFoundError:conda环境没启用或解释器选错

如果运行测试时出现ModuleNotFoundError: No module named 'pytest',第一反应不要是重装包,而是查解释器。在命令行里先确认当前环境:

conda activate projectX which python

然后在测试运行器的设置里找当前解释器路径,两者必须一致。我多次遇到过这种情况:命令行明明用的是 projectX,但编辑器界面里没有重新选中解释器,默认还是 base。这类问题很隐蔽,因为编辑器只会在状态栏里小字显示当前解释器,你不盯着看就会忽略。

5.2 测试收集不到用例:命名规则与testpaths

如果你运行 pytest 后提示“no tests ran”,那属于测试收集环节出了问题。按三个顺序排查:先看文件名和函数命名规则是否是 test_ 前缀;再看 pytest.ini 里的 testpaths 是否指向了实际目录;最后看测试文件是否放在了该目录下。还有一个在 Anaconda 场景下容易出现的坑:pytest 的 python_files 配置如果只写了*_test.py而没有写test_*.py,那么用 test_ 开头的文件反而不会被发现。配置规则是并集关系,要写全。这一点经常被忽略。

5.3 conda和pip混用的依赖冲突

Anaconda 环境里 conda 和 pip 混用是常态,但混用有风险。如果某一个包在 conda 源里版本太旧,你用 pip 装了新版本,下一次 conda install 新包时可能会把那个 pip 包降级。碰到这种问题,我建议用 conda list 查看每个包来源,如果发现某个包是 pip 装的且经常被动,优先把项目依赖收敛成单独的文件管理。在测试场景里,最稳妥的操作是:先通过 requirements-dev.txt 装 pytest 相关依赖,后续不要再对测试环境做额外的 conda install 大动作。

5.4 数据污染与测试相互影响

测试之间相互影响是个大坑,尤其当你测试的是会写文件、改全局配置的函数。解决方案很简单:能用 tmp_path 就不要自己建临时目录;能用 fixture 清理就不要在用例里手动 close。另外,fixture 的作用域也要留意。默认 fixture 每个用例执行一次,很适合轻量资源;如果 fixture 非常耗时,而且用例之间没有依赖,可以加大 scope 到 module 或 session,但代价是不同用例之间共享同一份状态,你得自己保证这个状态在只读场景下使用。

5.5 Jupyter里写测试的尴尬

经常有人问我:能不能直接在 Jupyter Notebook 里跑 pytest?我的建议是把测试代码写在 .py 文件里,让 pytest 正常运行,而不是在 notebook 里堆一堆 assert 然后手动看有没有报错。notebook 不是测试运行器的理想形态,它适合做探索性分析,不适合做可回归的测试套件。如果确实想在 notebook 里做快速验证,可以用子进程调用 pytest,把结果输出到 notebook 里看,但正式测试还是要依赖独立的测试文件。

5.6 一份快速排查清单

现象最可能原因快速定位办法
找不到 pytest 模块解释器不在当前环境在运行器里重新选择 conda 解释器
0 个用例运行命名或 testpaths 错误检查 pytest.ini 与文件名
导入项目模块失败测试目录不在 PYTHONPATH调整 testpaths 或使用 conftest.py
两次跑结果不同数据污染或依赖漂移使用 tmp_path、固定依赖版本
覆盖率命令报错pytest-cov 未装运行 pip install pytest-cov

6. 把测试跑起来之后,我还想再啰嗦几句

6.1 固定一条命令,比任何图形按钮都好用

配好 pytest 之后,我强烈建议你把它抽象成一条固定命令。团队里可以让所有人都用同一句:

conda run -n projectX pytest

如果项目更复杂,还可以写成 Makefile 的 target:

test: conda run -n projectX pytest -v

固定命令的好处是,换新机器、新同事加入、CI 环境跑测试,需要的知识都收敛为一条命令。图形界面的按钮适合自己开发时点着玩,但要可复制、可交接,还是命令行更牢靠。我见过不少团队,人一换,测试配置就在新同事手里折腾了一整天,最后发现只是解释器路径没对上。

6.2 测试带来的心态变化

最后分享一点直观感受:在 Anaconda 里把测试运行器彻底配顺之后,我写代码的心态完全不一样了。以前改一个工具函数,改完还要手动敲一堆命令验证,心里还发虚;现在写代码之前先想好测试用例,改完跑一下,绿了就大胆合入。这种安全感不是别人告诉你要写测试得来的,是你真正把运行器配顺、让反馈回路变得足够短之后自然产生的。所以我建议新手不要一上来就追求 100% 覆盖率,先挑项目中一个纯函数模块,写出它的核心用例,体验一下从配置到运行全流程畅通的感觉,再去扩展覆盖面。

我在实际配置中还有一个很小的习惯:把 pytest.ini 里的 addopts 保持固定,不要随手往里面加一堆临时的-s或者--no-header参数。临时参数用来调试可以,但一旦写进配置文件,它会一直影响后续所有测试,排查问题时还以为哪里逻辑写错了。如果非要临时用,在命令行里临时追加比改配置安全得多。这些细节看起来小,但长期积累下来,都能减少你突然觉得“测试结果不可信”的时刻。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进