4、pytest入门与嵌入式适配:从基础用法到conftest.py妙用
说实话,我刚开始做嵌入式测试那会儿,用的还是自己手写的测试脚本。每次换一个项目,就得重新搭一套测试框架,累得够呛。后来接触到 pytest,才发现原来测试可以这么清爽。这一章,我就带你从零开始,把 pytest 搬进嵌入式项目里。
4.1 pytest 到底是个啥?
pytest 是 Python 生态里最流行的测试框架之一。它不像 unittest 那么啰嗦,也不像 nose 那么老旧。说白了,它就是一个让你用最少的代码写测试的工具。
它的核心优势就三点:
- 自动发现测试:你只要按约定命名文件和函数,pytest 自己就能找到它们
- 断言简单:直接用 Python 的
assert就行,不用记一堆assertEqual、assertTrue - 插件生态丰富:覆盖率、参数化、并行执行,要啥有啥
我个人的习惯:不管项目大小,我都会用 pytest 做测试框架。哪怕只是一个裸机驱动,我也先搭好 pytest 环境再写代码。为什么?因为测试框架越早引入,后期返工越少。
4.2 pytest 基础用法:五分钟上手
先看一个最简单的例子。假设你要测试一个嵌入式设备上的加法函数:
# test_math.py
def test_add():
result = add(1, 2) # 假设这是从设备读取的结果
assert result == 3
def test_add_negative():
result = add(-1, -2)
assert result == -3
在终端里运行 pytest test_math.py,它就会自动执行这两个测试。如果通过,你会看到绿色的点;如果失败,它会告诉你哪一行断言没通过。
嗯,这里要注意:pytest 默认会递归查找当前目录下所有 test_*.py 或 *_test.py 文件,然后执行里面所有 test_* 开头的函数。这个约定非常方便,你不用手动注册任何东西。
4.3 嵌入式项目里怎么配 pytest?
嵌入式测试和纯软件测试最大的区别在于:测试代码跑在 PC 上,但被测对象是嵌入式设备。所以我们需要一套桥接机制。
我一般会在项目根目录下建一个 tests/ 文件夹,结构如下:
project_root/
├── tests/
│ ├── conftest.py
│ ├── test_uart.py
│ ├── test_gpio.py
│ └── test_sensor.py
├── firmware/ # 嵌入式固件源码
├── tools/ # 上位机工具
└── pytest.ini # pytest 配置文件
pytest.ini 文件长这样:
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
markers =
slow: 慢速测试,需要长时间运行
hardware: 需要真实硬件连接
unit: 纯软件单元测试,不需要硬件
为什么要加 markers?我在项目中遇到过这样的情况:有些测试需要连接真实硬件,有些只是模拟测试。用标记区分后,你可以只跑某类测试:
pytest -m "not hardware" # 跳过需要硬件的测试
pytest -m "slow" # 只跑慢速测试
小技巧:在嵌入式项目中,我建议把测试分成三层:单元测试(纯软件模拟)、集成测试(需要硬件)、系统测试(完整流程)。用 pytest 的标记机制,你可以灵活控制每次跑哪些测试。
4.4 conftest.py 的妙用
conftest.py 是 pytest 里最强大的文件之一。它就像一个全局配置中心,可以定义 fixture、钩子函数、自定义插件等。而且它是分层生效的——每个目录下的 conftest.py 只对该目录及其子目录生效。
我举个例子。在嵌入式测试中,最头疼的就是设备连接和资源管理。每次测试前要打开串口,测试完要关闭。用 conftest.py 可以完美解决:
# tests/conftest.py
import pytest
import serial
@pytest.fixture(scope="session")
def serial_port():
"""打开串口连接,整个测试会话只做一次"""
port = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
yield port
port.close()
@pytest.fixture
def reset_device(serial_port):
"""每次测试前复位设备"""
serial_port.write(b'reset\n')
response = serial_port.readline()
assert b'ready' in response
yield
你看,serial_port 这个 fixture 用了 scope="session",意思是整个测试过程只打开一次串口。而 reset_device 没有指定 scope,默认是 function,每个测试函数都会执行一次。
在测试文件里,你只需要这样用:
# tests/test_uart.py
def test_send_data(serial_port, reset_device):
serial_port.write(b'hello')
response = serial_port.readline()
assert response == b'OK\n'
def test_receive_data(serial_port, reset_device):
serial_port.write(b'read')
response = serial_port.readline()
assert len(response) > 0
fixture 的名字直接作为参数传入,pytest 会自动注入。这种依赖注入的方式,让测试代码变得非常干净。
我曾经踩过的坑:fixture 的 scope 设置不当会导致资源泄漏。比如串口连接如果设为 function,每个测试都打开关闭一次,不仅慢,还可能让设备来不及响应。我建议:硬件连接用 session,设备复位用 function,这样既高效又稳定。
4.5 conftest.py 的进阶用法
除了 fixture,conftest.py 还能做很多事。我挑几个嵌入式测试中常用的:
4.5.1 自定义命令行选项
有时候你需要指定串口号或波特率,硬编码在代码里太蠢了。可以在 conftest.py 里加自定义参数:
def pytest_addoption(parser):
parser.addoption(
"--port", action="store", default="/dev/ttyUSB0",
help="指定串口设备路径"
)
parser.addoption(
"--baud", action="store", type=int, default=115200,
help="指定波特率"
)
@pytest.fixture
def serial_port(request):
port_name = request.config.getoption("--port")
baud_rate = request.config.getoption("--baud")
port = serial.Serial(port_name, baud_rate, timeout=1)
yield port
port.close()
运行的时候就可以这样:
pytest --port /dev/ttyUSB1 --baud 9600
4.5.2 自动跳过某些测试
如果设备没连上,跑测试只会浪费时间。可以在 conftest.py 里加一个自动检查:
def pytest_collection_modifyitems(config, items):
"""如果设备不在线,跳过所有 hardware 标记的测试"""
try:
ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.5)
ser.close()
except serial.SerialException:
skip_hardware = pytest.mark.skip(reason="硬件未连接")
for item in items:
if "hardware" in item.keywords:
item.add_marker(skip_hardware)
这样一来,你不需要手动改任何测试代码。设备连上了就正常跑,没连上就自动跳过。我在项目中用这个模式,省了不少事。
4.6 知识结构总览
下面这张图总结了 pytest 在嵌入式测试中的核心脉络:
4.7 嵌入式测试的典型 fixture 清单
最后,我整理了一份嵌入式测试中常用的 fixture 清单,你可以直接拿来用:
| fixture 名称 | scope | 作用 | 典型实现 |
|---|---|---|---|
serial_port |
session | 管理串口连接 | pyserial 打开/关闭 |
tcp_client |
session | 管理 TCP 连接 | socket 连接/断开 |
reset_device |
function | 每次测试前复位设备 | 发送复位指令并等待就绪 |
flash_firmware |
session | 烧录最新固件 | 调用烧录工具 |
log_collector |
function | 采集测试过程中的日志 | 读取串口或文件日志 |
test_data |
function | 生成测试数据 | 随机数或预设数据集 |
我的建议:刚开始用 pytest 做嵌入式测试时,别想着一步到位。先搭一个最简单的 conftest.py,只放一个串口 fixture。跑通一个测试后,再慢慢加其他 fixture。这样出了问题,你也能快速定位。