4、单元测试框架:Pytest入门、TestNG基础、测试用例编写规范

单元测试,说白了就是给代码做「体检」。你写了一个驱动函数,它到底能不能跑?边界情况处理了没?内存泄漏了没?这些不能靠猜,得靠测试用例来验证。

我个人习惯,在驱动开发中把单元测试当作「安全网」。没有这张网,你重构代码时心里是发虚的。今天咱们就聊聊两个主流框架——Pytest 和 TestNG,以及怎么写出一手漂亮的测试用例。

核心观点:测试不是负担,是驱动代码质量的「第一道防线」。尤其是驱动集成依赖验证,一个用例没覆盖到,线上可能就崩了。

4.1 Pytest 入门:Python 世界的测试利器

Pytest 是我在 Python 项目中最常用的框架。为什么?因为它简单、灵活,而且插件生态丰富。你想想看,一个测试框架如果能做到「零配置启动」,那上手成本就极低了。

先看一个最简单的例子:

# test_driver.py
def test_open_device():
    """测试打开设备驱动"""
    result = open_device("/dev/ttyUSB0")
    assert result == True, "设备打开失败"

def test_read_data():
    """测试读取数据"""
    data = read_data(1024)
    assert len(data) > 0, "读取数据为空"
    assert isinstance(data, bytes), "数据类型错误"

运行方式也简单,终端里敲 pytest 就行。它会自动发现所有以 test_ 开头的函数,然后执行。

我在项目中遇到过一个问题:驱动测试往往需要硬件环境,比如串口设备、USB 设备。如果硬件没接,测试直接跑会报错。这时候可以用 Pytest 的 skipif 装饰器:

import pytest
import os

@pytest.mark.skipif(
    not os.path.exists("/dev/ttyUSB0"),
    reason="硬件设备未连接"
)
def test_serial_communication():
    """串口通信测试,硬件不在就跳过"""
    assert serial_send("AT") == "OK"

嗯,这里要注意:skipif 不是逃避测试,而是让测试更智能。你想想看,如果 CI 环境没有硬件,测试直接失败,那整个流水线就断了。跳过并记录原因,才是专业做法。

Pytest 的 Fixture 机制

Fixture 是 Pytest 的精髓。说白了,它就是「测试前的准备工作」和「测试后的清理工作」。比如打开设备、分配内存、建立连接,这些重复劳动可以统一管理。

import pytest

@pytest.fixture
def serial_device():
    """打开串口设备,测试结束后自动关闭"""
    device = open_serial("/dev/ttyUSB0", baudrate=115200)
    yield device  # 测试执行时返回设备对象
    device.close()  # 测试结束后清理

def test_send_command(serial_device):
    """使用 fixture 进行测试"""
    response = serial_device.send("AT+STATUS")
    assert "OK" in response

我个人习惯把 Fixture 放在 conftest.py 文件中,这样多个测试文件可以共享。你想想看,如果每个测试文件都写一遍打开设备的逻辑,那维护起来多痛苦。

小技巧:Pytest 的 -v 参数可以显示详细输出,-k 参数可以按名称筛选测试用例。比如 pytest -v -k "serial" 只运行名字包含 serial 的用例。

4.2 TestNG 基础:Java 驱动的测试框架

如果你做 Android 驱动开发或者 Java 后端,TestNG 是绕不开的。它比 JUnit 更灵活,尤其是依赖测试和参数化测试方面。

先看一个 TestNG 的基本结构:

import org.testng.annotations.Test;
import org.testng.Assert;

public class DriverTest {
    
    @Test
    public void testOpenDevice() {
        Driver driver = new Driver();
        boolean result = driver.open("/dev/i2c-1");
        Assert.assertTrue(result, "I2C设备打开失败");
    }
    
    @Test(dependsOnMethods = {"testOpenDevice"})
    public void testReadRegister() {
        Driver driver = new Driver();
        int value = driver.readRegister(0x10);
        Assert.assertNotNull(value, "寄存器读取为空");
    }
}

看到 dependsOnMethods 了吗?这是 TestNG 的特色。它允许你定义测试用例之间的依赖关系。比如「先打开设备,再读取寄存器」,如果打开失败了,读取寄存器的测试会自动跳过,不会报一堆无意义的错误。

我曾经在一个驱动项目中吃过亏:测试用例之间没有依赖关系,结果打开设备失败后,后面 20 个用例全红了。排查了半天才发现是前置条件没满足。后来改用 TestNG 的依赖机制,问题迎刃而解。

TestNG 的参数化测试

驱动测试中,经常需要测试不同参数下的行为。比如不同的波特率、不同的寄存器地址。TestNG 的 @DataProvider 就是干这个的:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class SerialTest {
    
    @DataProvider(name = "baudrateProvider")
    public Object[][] provideBaudrates() {
        return new Object[][] {
            {9600, true},
            {19200, true},
            {115200, true},
            {999999, false}  // 非法波特率,预期失败
        };
    }
    
    @Test(dataProvider = "baudrateProvider")
    public void testBaudrate(int baudrate, boolean expected) {
        SerialDevice device = new SerialDevice();
        boolean result = device.setBaudrate(baudrate);
        Assert.assertEquals(result, expected, 
            "波特率 " + baudrate + " 测试失败");
    }
}

你想想看,如果不用参数化,你得写四个独立的测试函数,代码冗余不说,维护起来也麻烦。参数化测试让数据和逻辑分离,这才是专业做法。

注意:TestNG 的 @Test 注解默认是「无序执行」的。如果你需要指定顺序,要么用 dependsOnMethods,要么用 priority 属性。千万别依赖代码书写顺序,那是不靠谱的。

4.3 测试用例编写规范

框架只是工具,真正决定测试质量的是用例本身。我见过太多人写测试用例,要么太简单(只测正常路径),要么太复杂(一个用例测了十件事)。

下面是我总结的几个核心规范,也是我在团队中强制执行的:

规范一:一个用例只测一件事

这是最基础、也最容易被违反的规范。一个测试函数只验证一个行为。比如:

  • 错误示范:一个函数里既测试打开设备,又测试读写数据,还测试关闭设备。
  • 正确做法:拆成 test_open_device、test_read_data、test_close_device 三个独立用例。

为什么?因为如果测试失败了,你得立刻知道是哪个环节出了问题。一个用例测多件事,排查起来就像大海捞针。

规范二:使用 Arrange-Act-Assert 模式

这是测试用例的标准结构,说白了就是三步走:

  1. Arrange(准备):创建对象、初始化数据、建立连接。
  2. Act(执行):调用被测函数,执行核心逻辑。
  3. Assert(断言):验证结果是否符合预期。

看个例子:

def test_write_register():
    # Arrange: 准备设备和数据
    device = I2CDevice("/dev/i2c-1", address=0x50)
    device.open()
    expected_data = b"\x01\x02\x03"
    
    # Act: 执行写操作
    result = device.write(0x10, expected_data)
    
    # Assert: 验证结果
    assert result == True
    assert device.read(0x10, 3) == expected_data

我个人习惯在代码里用空行把这三段隔开,一眼就能看出结构。你想想看,如果所有代码挤在一起,别人 review 的时候得多痛苦。

规范三:测试用例要可重复、无副作用

驱动测试经常涉及硬件状态。比如你写了一个寄存器,测试结束后要恢复原值。否则第二次运行测试时,环境已经变了,结果就不准了。

我曾经遇到过一个问题:一个同事写的测试用例,第一次跑全绿,第二次跑就红了。排查了半天,发现是测试用例修改了全局状态,但没有恢复。从那以后,我要求所有测试用例必须「自包含」——自己准备环境,自己清理环境。

最佳实践:使用 Fixture 或 @AfterMethod 做清理工作。比如 TestNG 的 @AfterMethod 注解,Pytest 的 yield fixture,都是干这个的。

规范四:命名要清晰,断言要具体

测试用例的名字就是文档。别人看到 test_write_register_with_invalid_address,立刻就知道这个用例在测什么。别用 test_1、test_case_2 这种名字,那是给自己挖坑。

断言信息也要具体。比如:

# 不好的断言
assert result == True

# 好的断言
assert result == True, f"写入寄存器 0x10 失败,返回值为 {result}"

你想想看,如果断言失败了,没有错误信息,你得去翻代码才能知道哪里出了问题。加上具体的错误信息,排查效率能提升好几倍。

4.4 知识体系总览

下面这张图是我整理的单元测试知识体系,涵盖了框架选择、用例规范和常见模式。你可以把它当作一个「速查地图」:

单元测试知识体系总览 Pytest(Python) • 自动发现测试用例 • Fixture 机制 • skipif 条件跳过 • 参数化测试 • conftest.py 共享 TestNG(Java) • 依赖测试机制 • DataProvider 参数化 • @Before/@After 注解 • 分组测试 • 并行执行 编写规范 • 一个用例测一件事 • Arrange-Act-Assert • 可重复无副作用 • 命名清晰断言具体 • 自包含环境 驱动测试中的常见模式 1. 硬件模拟模式:使用 Mock 对象模拟硬件响应 2. 状态验证模式:测试前后对比寄存器/内存状态 3. 边界测试模式:测试最大/最小/非法参数 4. 异常恢复模式:模拟硬件断开后重新连接 5. 并发测试模式:多线程同时访问驱动接口 应用

这张图把 Pytest 和 TestNG 的核心特性放在左边和中间,右边是编写规范,底部是驱动测试中常见的五种模式。我个人建议你把它打印出来贴在工位上,写测试用例的时候对照着看,能少踩很多坑。

我的建议:刚开始写测试用例时,别追求覆盖率 100%。先覆盖核心路径和边界条件,等代码稳定了再逐步补充。一口吃不成胖子,测试也是迭代出来的。

好了,关于单元测试框架和用例规范,今天就聊这么多。记住一点:测试不是写给领导看的,是写给你自己看的。好的测试用例,能让你在凌晨三点被叫起来修 bug 时,少掉几根头发。

可可精品资料库