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 在嵌入式测试中的核心脉络:

pytest 嵌入式测试框架核心结构 pytest 测试引擎 测试发现与执行 Fixture 依赖注入 标记与配置 自动发现 test_* 参数化测试 scope 控制 yield 资源管理 自定义标记 conftest.py 配置中心 串口/网络通信 设备复位 | 固件烧录 | 日志采集 | 结果比对 嵌入式 适配层

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。这样出了问题,你也能快速定位。