2、pytest快速上手:pytest安装与配置、断言机制、测试用例编写规范、运行模式

好,咱们直接进入正题。pytest 这东西,说白了就是 Python 测试界的瑞士军刀。我最早用 unittest 的时候,写个测试用例得继承 TestCase,还得记一堆 assertEqual、assertTrue 的方法名,烦得很。后来换成 pytest,第一感觉就是——这才叫「写代码」嘛。

2.1 安装与配置

安装没什么好说的,一行命令搞定:

pip install pytest

我个人习惯在项目里创建一个 requirements-dev.txt,把 pytest 和它的插件都写进去。这样团队成员拉下来直接装,省得扯皮。

验证安装是否成功,跑一下:

pytest --version

嗯,看到版本号就说明成了。

配置方面,pytest 支持 pytest.ini、pyproject.toml、tox.ini 这些文件。我一般用 pytest.ini,简单直接:

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short

这里我解释一下几个关键配置:

  • testpaths:告诉 pytest 去哪找测试文件。默认是当前目录,但我习惯把测试都扔到 tests/ 文件夹里。
  • python_files:匹配测试文件的命名规则。默认就是 test_*.py,你也可以改成 *_test.py,看团队习惯。
  • addopts:每次运行都自动加上的参数。-v 是详细输出,--tb=short 是缩短错误堆栈。我刚开始用 pytest 时,错误堆栈能刷一屏幕,看得眼花。后来加了 --tb=short,清爽多了。
小提示:如果你在 Windows 上跑 pytest,遇到编码问题,可以在 pytest.ini 里加一行 filterwarnings = ignore::DeprecationWarning,能省不少心。

2.2 断言机制

pytest 的断言,说白了就是 Python 原生的 assert 关键字。你想想看,不用记那些乱七八糟的 assertEqual、assertTrue,直接写 assert a == b 就行了。多舒服。

来看几个例子:

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

def test_string():
    name = "pytest"
    assert "py" in name

def test_list():
    items = [1, 2, 3]
    assert len(items) == 3
    assert 2 in items

def test_float():
    value = 0.1 + 0.2
    assert abs(value - 0.3) < 1e-6  # 浮点数比较要小心

这里有个坑,我必须要说一下。浮点数比较,千万别直接 assert 0.1 + 0.2 == 0.3。为什么?因为计算机里浮点数有精度问题。0.1 + 0.2 实际上等于 0.30000000000000004。我曾经在一个财务系统里吃过这个亏,算出来的金额差了一分钱,查了半天才发现是浮点数比较的问题。

pytest 也提供了更高级的断言用法,比如异常断言:

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        result = 1 / 0

这个 pytest.raises 特别好用。你想想看,如果你要测试一个函数在输入非法参数时会不会抛出异常,用这个就对了。

注意:断言失败时,pytest 会给出非常详细的错误信息。它会告诉你期望值和实际值分别是什么,甚至会用 ^ 符号标出哪里不匹配。这个特性在调试时特别有用。

2.3 测试用例编写规范

写测试用例,说白了就是写给人看的。我见过不少团队,测试代码写得比生产代码还乱,最后没人愿意维护。所以,规范很重要。

pytest 的测试用例编写,遵循几个简单的规则:

  • 文件名:以 test_ 开头,比如 test_user.py
  • 类名:以 Test 开头,比如 TestUserAPI
  • 函数名:以 test_ 开头,比如 test_create_user

来看一个完整的例子:

# tests/test_user.py

class TestUserAPI:
    def test_create_user(self):
        """测试创建用户接口"""
        # 准备数据
        user_data = {"name": "张三", "age": 25}
        
        # 执行操作
        result = create_user(user_data)
        
        # 验证结果
        assert result["status"] == "success"
        assert result["user_id"] is not None

    def test_create_user_with_empty_name(self):
        """测试创建用户时姓名为空"""
        user_data = {"name": "", "age": 25}
        
        with pytest.raises(ValueError, match="姓名不能为空"):
            create_user(user_data)

我个人习惯在每个测试函数上加一个 docstring,说明这个测试是测什么的。为什么?因为半年后你回来看代码,可能完全想不起来这个 test_create_user 到底在测什么。docstring 就是给你自己看的备忘录。

还有一个规范,叫「三A原则」:

  1. Arrange(准备):准备好测试数据和环境
  2. Act(执行):执行被测试的代码
  3. Assert(断言):验证结果是否符合预期

你看上面的例子,我就是按这个顺序写的。代码里用空行隔开,一目了然。

核心原则:一个测试用例只测一个功能点。如果你发现一个测试函数里写了三个 assert,而且它们测的是不同的事情,那就拆成三个测试函数。

2.4 运行模式

pytest 的运行模式非常灵活。我平时最常用的几种方式:

命令 说明
pytest 运行所有测试
pytest tests/ 运行指定目录下的测试
pytest test_user.py 运行指定文件
pytest test_user.py::TestUserAPI 运行指定测试类
pytest test_user.py::TestUserAPI::test_create_user 运行指定测试函数
pytest -k "create" 按名称匹配运行(包含"create"的测试)
pytest -m "slow" 按标记运行(需要先给测试加 @pytest.mark.slow)

这里我重点说一下 -k 参数。它支持表达式匹配,比如:

pytest -k "create or update"  # 运行名称包含 create 或 update 的测试
pytest -k "not delete"         # 运行名称不包含 delete 的测试

这个功能在调试时特别有用。比如你改了一个函数,想看看相关的测试有没有全过,直接用 -k 匹配函数名就行了。

还有一个我经常用的参数是 -x,遇到第一个失败就停止:

pytest -x

为什么用这个?因为有时候测试跑得慢,你不想等全部跑完才发现第一个就挂了。先修好第一个,再跑剩下的。

最后,说说 --pdb 参数。这个参数会在测试失败时自动进入调试模式:

pytest --pdb

嗯,这个我强烈推荐。遇到复杂的失败场景,直接进调试器看变量值,比看错误堆栈快多了。

我的习惯:开发阶段用 pytest -v --tb=short,快速定位问题。提交代码前用 pytest -v --tb=long 跑一遍完整测试,确保没有遗漏。

好了,pytest 的基础就这些。下一章我们会深入 fixture 和参数化,那才是 pytest 真正强大的地方。先把这个基础打牢,后面学起来就顺了。