4. pytest入门:安装与配置、断言机制、fixture基础、参数化测试

说到自动化测试框架,Python 生态里 pytest 几乎成了事实标准。我最早接触它是在一个遗留项目上,当时团队还在用 unittest,用例写得又长又啰嗦。后来我试着把几个核心模块切到 pytest,嗯,真香。今天我们就从零开始,把 pytest 的这几个核心能力吃透。

4.1 安装与配置

安装没什么门槛,一行命令搞定:

pip install pytest

我个人习惯在项目里创建一个 pytest.ini 配置文件,把常用参数固化下来。这样团队其他人拉下代码就能跑,不用记一堆命令行参数。

# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    smoke: 冒烟测试
    regression: 回归测试
    slow: 慢速测试
小技巧:配置文件中 testpaths 指定测试目录,markers 注册自定义标签。我习惯把冒烟测试标记为 smoke,这样 CI 流程里可以快速筛选执行。

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

pytest --version

看到版本号就说明环境搭好了。你想想看,就这么简单,一个工具就能撑起整个质量校验体系。

4.2 断言机制

pytest 的断言比 unittest 清爽太多。它直接用 Python 原生的 assert 关键字,不需要记 assertEqual、assertTrue 那一套 API。

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

def test_string_contains():
    text = "hello pytest"
    assert "pytest" in text

def test_list_not_empty():
    items = [1, 2, 3]
    assert len(items) > 0

断言失败时,pytest 会给出非常详细的上下文信息。我记得有一次排查一个接口返回的 JSON 结构问题,pytest 直接把实际值和期望值的差异打印出来了,省了我不少调试时间。

核心要点:pytest 的断言本质就是 Python 的 assert,但它在背后做了大量工作——重写断言表达式,让你看到失败时的具体差异。

对于复杂数据结构的校验,我常用这种写法:

def test_api_response():
    response = {"status": 200, "data": {"name": "test", "count": 10}}
    expected = {"status": 200, "data": {"name": "test", "count": 10}}
    assert response == expected

为什么会这样?因为 pytest 重写了 == 操作符的报错信息,嵌套再深也能把差异点标出来。

4.3 Fixture 基础

Fixture 是 pytest 最强大的特性之一,没有之一。说白了,它就是用来准备测试数据和环境的。

先看一个最简单的例子:

import pytest

@pytest.fixture
def sample_data():
    return {"id": 1, "name": "test_user"}

def test_user_name(sample_data):
    assert sample_data["name"] == "test_user"

fixture 函数通过 @pytest.fixture 装饰器标记,测试函数直接把它当参数用。pytest 会自动注入。

fixture 的作用域有四种,我整理了一张表:

作用域 生命周期 适用场景
function 每个测试函数执行前后 临时数据、mock 对象
class 每个测试类执行前后 类级别的共享资源
module 每个测试文件执行前后 数据库连接、配置加载
session 整个测试会话期间 全局配置、第三方服务客户端

我曾经在一个项目中踩过坑:fixture 默认是 function 作用域,但我在一个模块里写了 50 个测试用例,每个用例都重新创建数据库连接,测试跑了 20 分钟。后来改成 module 作用域,时间直接降到 3 分钟。

注意:作用域越大,fixture 的副作用风险也越大。session 级别的 fixture 如果修改了全局状态,会影响所有测试用例。我建议优先用 function 和 module,除非你很清楚自己在做什么。

fixture 还支持 yield 实现 teardown 逻辑:

@pytest.fixture
def db_connection():
    conn = create_connection()
    yield conn
    conn.close()  # 测试结束后自动清理

这种写法特别适合需要「用完即焚」的资源,比如临时文件、数据库连接、网络 socket。

4.4 参数化测试

参数化测试解决的是「同一套逻辑,多组数据」的问题。没有参数化的时候,你可能会写出这样的代码:

def test_add_positive():
    assert add(1, 2) == 3

def test_add_negative():
    assert add(-1, -2) == -3

def test_add_zero():
    assert add(0, 0) == 0

看着就累,对吧?用 @pytest.mark.parametrize 可以写成这样:

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (-1, -2, -3),
    (0, 0, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

一个测试函数覆盖了四组数据。如果某组数据失败,pytest 会精确告诉你哪组数据出了问题。

参数化还可以叠加使用,生成笛卡尔积:

@pytest.mark.parametrize("username", ["admin", "user", "guest"])
@pytest.mark.parametrize("password", ["123456", "password", "admin123"])
def test_login(username, password):
    result = login(username, password)
    assert result is not None

这样会生成 3×3=9 个测试用例。我在做权限系统测试时经常用这种组合,能快速覆盖各种用户角色和密码组合。

实战建议:参数化数据量大的时候,可以单独抽成一个列表变量,或者从外部文件(JSON/YAML)读取。我习惯把测试数据放在 tests/data/ 目录下,用 fixture 加载,这样数据和代码分离,维护起来更清爽。

4.5 知识体系总览

下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:

pytest 入门知识体系 pytest 核心 安装与配置 断言机制 Fixture 基础 参数化测试 pip install pytest.ini 配置 原生 assert 关键字 @pytest.fixture @pytest.mark.parametrize 💡 学习路径建议 先掌握安装配置 → 学会写断言 → 用 fixture 管理资源 → 用参数化减少重复代码

这张图把四个知识模块的关系理清楚了。安装配置是基础,断言是验证手段,fixture 管理测试资源,参数化减少重复代码。它们组合在一起,就是一套完整的测试框架。