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 直接把实际值和期望值的差异打印出来了,省了我不少调试时间。
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 分钟。
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 个测试用例。我在做权限系统测试时经常用这种组合,能快速覆盖各种用户角色和密码组合。
tests/data/ 目录下,用 fixture 加载,这样数据和代码分离,维护起来更清爽。
4.5 知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:
这张图把四个知识模块的关系理清楚了。安装配置是基础,断言是验证手段,fixture 管理测试资源,参数化减少重复代码。它们组合在一起,就是一套完整的测试框架。