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 模式
这是测试用例的标准结构,说白了就是三步走:
- Arrange(准备):创建对象、初始化数据、建立连接。
- Act(执行):调用被测函数,执行核心逻辑。
- 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 和 TestNG 的核心特性放在左边和中间,右边是编写规范,底部是驱动测试中常见的五种模式。我个人建议你把它打印出来贴在工位上,写测试用例的时候对照着看,能少踩很多坑。
我的建议:刚开始写测试用例时,别追求覆盖率 100%。先覆盖核心路径和边界条件,等代码稳定了再逐步补充。一口吃不成胖子,测试也是迭代出来的。
好了,关于单元测试框架和用例规范,今天就聊这么多。记住一点:测试不是写给领导看的,是写给你自己看的。好的测试用例,能让你在凌晨三点被叫起来修 bug 时,少掉几根头发。
可可精品资料库