4. Pytest 入门:断言机制、测试用例编写、Fixture 基础、conftest.py 共享配置
好,咱们进入第四章。前面几章我们把 Python 基础和环境搭好了,现在开始真正写测试脚本。
Pytest 是目前 Python 生态里最主流的测试框架。我这些年做嵌入式测试,从早期的 unittest 到后来的 nose,最后全切到了 pytest。为什么?因为它简洁、灵活,而且插件生态太丰富了。你想想看,一个框架能同时搞定单元测试、集成测试、甚至硬件接口测试,这得多省事。
4.1 断言机制:测试的灵魂
测试的本质是什么?说白了就是「比较预期和实际」。Pytest 的断言用起来特别自然——就是 Python 原生的 assert 关键字。
核心原则: 测试用例中只要 assert 后面的表达式为 True,用例就通过;为 False,就失败。
4.1.1 基本断言写法
# test_assert_basic.py
def test_addition():
result = 1 + 1
assert result == 2
def test_string():
name = "hello"
assert "he" in name
def test_boolean():
flag = True
assert flag is True
你看,就这么简单。不需要像 unittest 那样记一堆 self.assertEqual() 的方法名。
4.1.2 常用断言场景
| 场景 | 断言写法 | 说明 |
|---|---|---|
| 相等判断 | assert a == b |
最常用,比较值 |
| 不等判断 | assert a != b |
确认两者不同 |
| 包含判断 | assert x in y |
字符串、列表、字典都适用 |
| 布尔判断 | assert x is True |
注意用 is 而不是 == |
| 异常判断 | with pytest.raises(ValueError) |
确认代码抛出了指定异常 |
| 近似判断 | assert abs(a - b) < 1e-6 |
浮点数比较,避免精度问题 |
我的经验: 嵌入式测试里经常要比较浮点数,比如 ADC 采样值。直接用 assert a == b 很容易踩坑。我习惯用 pytest.approx(),它内置了容差处理。
# 浮点数比较的正确姿势
def test_float():
adc_value = 3.1415926
expected = 3.14
assert adc_value == pytest.approx(expected, rel=1e-2) # 相对误差 1%
4.1.3 异常断言
有时候我们需要测试「代码应该抛出异常」的场景。比如读取一个不存在的文件,或者传入非法参数。
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
result = 1 / 0
def test_custom_exception():
with pytest.raises(ValueError, match="参数不能为负数"):
validate_input(-1)
这里 match 参数可以匹配异常信息中的关键字,非常实用。
4.2 测试用例编写:从单测到套件
Pytest 的用例编写有几个约定,记住就行:
- 测试文件以
test_开头,或者以_test结尾 - 测试函数以
test_开头 - 测试类以
Test开头,且不能有__init__方法
4.2.1 函数式用例
# test_sensor.py
def test_temperature_read():
"""测试温度传感器读取"""
sensor = TemperatureSensor()
value = sensor.read()
assert 20 <= value <= 30
def test_humidity_read():
"""测试湿度传感器读取"""
sensor = HumiditySensor()
value = sensor.read()
assert 40 <= value <= 60
4.2.2 类式用例
当多个测试用例共享相同的初始化逻辑时,用类组织更清晰。
class TestSensorSuite:
"""传感器测试套件"""
def test_temperature(self):
sensor = TemperatureSensor()
assert sensor.init() == True
def test_humidity(self):
sensor = HumiditySensor()
assert sensor.init() == True
def test_pressure(self):
sensor = PressureSensor()
assert sensor.init() == True
注意: 类里的每个 test_ 方法都是独立执行的。不要依赖方法之间的执行顺序,也不要共享变量状态。我曾经见过有人把测试结果存在类变量里,结果并行执行时全乱了。
4.3 Fixture 基础:测试的「前置条件」管理
Fixture 是 pytest 最强大的特性之一。它解决了测试中「准备数据、创建对象、清理资源」这些重复劳动。
4.3.1 最简单的 Fixture
import pytest
@pytest.fixture
def sensor():
"""创建一个传感器实例"""
s = TemperatureSensor()
s.connect()
yield s
s.disconnect()
def test_sensor_read(sensor):
"""使用 fixture 作为参数"""
value = sensor.read()
assert value is not None
这里 yield 之前的代码是「前置准备」,之后的代码是「后置清理」。我特别喜欢这种写法,比 unittest 的 setUp/tearDown 直观多了。
4.3.2 Fixture 的作用域
| 作用域 | 生命周期 | 适用场景 |
|---|---|---|
function |
每个测试函数执行一次 | 默认值,适合独立资源 |
class |
每个测试类执行一次 | 类级别的共享资源 |
module |
每个模块执行一次 | 模块级别的配置加载 |
session |
整个测试会话执行一次 | 数据库连接、硬件初始化 |
@pytest.fixture(scope="session")
def hardware_board():
"""整个测试过程只初始化一次硬件"""
board = Board()
board.power_on()
yield board
board.power_off()
我的建议: 嵌入式测试里,硬件初始化通常很慢。用 scope="session" 可以大幅节省时间。但要注意——如果硬件状态会被测试用例修改,就别用 session 级别,否则用例之间会互相影响。
4.3.3 Fixture 的自动使用
有些 fixture 你希望每个测试都自动执行,比如记录日志、检查环境。
@pytest.fixture(autouse=True)
def check_environment():
"""每个测试前自动检查环境"""
assert is_board_connected(), "硬件未连接"
yield
print("环境检查完成")
加上 autouse=True 后,这个 fixture 会自动应用到所有测试用例,不需要显式声明参数。
4.4 conftest.py:共享配置的「中央枢纽」
conftest.py 是 pytest 的「魔法文件」。放在某个目录下的 conftest.py,会自动被该目录及其子目录下的所有测试文件共享。
4.4.1 目录结构示例
project/
├── conftest.py # 全局 fixture
├── tests/
│ ├── conftest.py # tests 目录共享 fixture
│ ├── test_sensor.py
│ └── sub_tests/
│ ├── conftest.py # sub_tests 目录共享 fixture
│ └── test_advanced.py
4.4.2 conftest.py 里写什么
# conftest.py (项目根目录)
import pytest
@pytest.fixture(scope="session")
def global_config():
"""全局配置,所有测试共享"""
return {
"baudrate": 115200,
"timeout": 5,
"log_level": "DEBUG"
}
@pytest.fixture
def serial_port(global_config):
"""串口连接 fixture"""
import serial
port = serial.Serial(
port="/dev/ttyUSB0",
baudrate=global_config["baudrate"],
timeout=global_config["timeout"]
)
yield port
port.close()
然后在任意测试文件里,直接使用 serial_port 这个 fixture:
# tests/test_uart.py
def test_uart_send(serial_port):
serial_port.write(b"AT\r\n")
response = serial_port.readline()
assert b"OK" in response
关键点: conftest.py 不需要被 import。pytest 会自动发现它。你只需要把 fixture 定义在里面,测试文件就能直接用。
4.4.3 覆盖与优先级
如果多个 conftest.py 定义了同名的 fixture,离测试文件最近的会覆盖远的。这个机制很灵活,你可以:
- 在根 conftest.py 定义通用 fixture
- 在子目录 conftest.py 定义专用 fixture
- 在测试文件里直接定义局部 fixture
避坑指南: 我曾经在项目里把 fixture 定义在 conftest.py 里,但忘记加 scope 参数。结果每个测试都重新创建数据库连接,跑 200 个用例花了 10 分钟。加上 scope="session" 后,直接降到 30 秒。所以,fixture 的作用域一定要想清楚。
4.5 本章知识体系
下面这张图总结了本章的核心内容,你可以把它当作速查表:
4.6 小结
这一章我们覆盖了 pytest 最核心的四个概念:
- 断言机制:用 Python 原生的 assert,加上 pytest 的异常断言和近似断言
- 测试用例:函数式和类式两种写法,记住命名约定
- Fixture:管理测试的前置条件和后置清理,注意作用域的选择
- conftest.py:共享配置的中央枢纽,自动发现,层级覆盖