3. Python单元测试框架unittest入门:TestCase、断言方法、测试固件
说到Python的单元测试,unittest是标准库自带的框架,不需要额外安装。我个人习惯把它当作嵌入式测试的"第一道防线"——在把代码烧到板子上之前,先用unittest把逻辑跑通。
你可能会问:嵌入式开发为什么要学Python的测试框架?嗯,原因很简单。我们的测试脚本、仿真控制脚本、数据处理脚本,大部分都是用Python写的。这些脚本如果出了问题,整个开发流程都会卡住。unittest就是用来保证这些脚本质量的工具。
3.1 TestCase:测试的基本单元
TestCase是unittest的核心。说白了,一个TestCase就是一个测试用例类。你写的每个测试方法,都对应一个具体的测试场景。
来看个最简单的例子:
import unittest
class TestMathOperations(unittest.TestCase):
def test_addition(self):
result = 1 + 1
self.assertEqual(result, 2)
def test_subtraction(self):
result = 5 - 3
self.assertEqual(result, 2)
if __name__ == '__main__':
unittest.main()
运行这段代码,unittest会自动发现所有以test_开头的方法,逐个执行。我在项目中遇到过不少新手把测试方法写成def test(self):,结果只跑了一个测试——记住,方法名必须以test_开头,框架才会识别。
test_uart_send_returns_correct_bytes。这样测试失败时,从名字就能猜到问题出在哪。
3.2 断言方法:验证结果的武器库
断言是测试的灵魂。unittest提供了丰富的断言方法,我整理了一张表,方便你查阅:
| 断言方法 | 用途 | 示例 |
|---|---|---|
assertEqual(a, b) |
检查 a == b | self.assertEqual(calc.add(1,2), 3) |
assertNotEqual(a, b) |
检查 a != b | self.assertNotEqual(result, None) |
assertTrue(x) |
检查 x 为 True | self.assertTrue(flag.is_set()) |
assertFalse(x) |
检查 x 为 False | self.assertFalse(error_occurred) |
assertIsNone(x) |
检查 x 为 None | self.assertIsNone(cache.get(key)) |
assertIsNotNone(x) |
检查 x 不为 None | self.assertIsNotNone(device.read()) |
assertIn(a, b) |
检查 a in b | self.assertIn('error', log_text) |
assertRaises(Exc, fun) |
检查是否抛出异常 | self.assertRaises(ValueError, parse, 'abc') |
我曾经在调试一个串口驱动时,用assertEqual比对发送和接收的数据,发现总是对不上。后来换成assertIn检查关键字段,才定位到是协议头解析出了问题。选对断言方法,能帮你更快找到问题。
assertTrue。比如检查两个值相等,用assertEqual比assertTrue(a == b)好得多——前者在失败时会打印出a和b的具体值,后者只会告诉你"False"。
3.3 测试固件:setUp和tearDown
测试固件(Fixture)是unittest中一个非常实用的机制。它解决了一个常见问题:多个测试方法需要相同的初始化或清理操作。
举个例子,假设我们要测试一个LED控制器的Python封装:
import unittest
class TestLEDController(unittest.TestCase):
def setUp(self):
# 每个测试方法执行前都会运行
self.led = LEDController(pin=17)
self.led.off()
print("LED已初始化")
def tearDown(self):
# 每个测试方法执行后都会运行
self.led.off()
print("LED已关闭")
def test_turn_on(self):
self.led.on()
self.assertTrue(self.led.is_on())
def test_turn_off(self):
self.led.on()
self.led.off()
self.assertFalse(self.led.is_on())
def test_toggle(self):
self.led.toggle()
self.assertTrue(self.led.is_on())
self.led.toggle()
self.assertFalse(self.led.is_on())
执行流程是这样的:
setUp()→test_turn_on()→tearDown()setUp()→test_turn_off()→tearDown()setUp()→test_toggle()→tearDown()
每个测试方法都是独立的,互不干扰。我在项目中遇到过最典型的坑是:有人在setUp里打开了文件,但在tearDown里忘了关闭。跑一次测试没问题,跑一百次就报"打开文件太多"的错误。所以,tearDown一定要把setUp里申请的资源都释放掉。
3.4 知识结构图
下面这张图展示了unittest测试框架的核心结构:
3.5 实战:测试一个简单的配置解析器
把上面的知识点串起来,写一个完整的测试用例。假设我们有一个解析嵌入式设备配置文件的函数:
# config_parser.py
def parse_config_line(line):
"""解析形如 'KEY=VALUE' 的配置行"""
line = line.strip()
if not line or line.startswith('#'):
return None
if '=' not in line:
raise ValueError(f"无效配置行: {line}")
key, value = line.split('=', 1)
return key.strip(), value.strip()
对应的测试用例:
import unittest
from config_parser import parse_config_line
class TestConfigParser(unittest.TestCase):
def setUp(self):
# 准备测试数据
self.valid_line = "BAUD_RATE=115200"
self.comment_line = "# 这是注释"
self.empty_line = " "
self.invalid_line = "NO_EQUALS"
def test_parse_valid_line(self):
result = parse_config_line(self.valid_line)
self.assertIsNotNone(result)
self.assertEqual(result, ("BAUD_RATE", "115200"))
def test_parse_comment_line(self):
result = parse_config_line(self.comment_line)
self.assertIsNone(result)
def test_parse_empty_line(self):
result = parse_config_line(self.empty_line)
self.assertIsNone(result)
def test_parse_invalid_line_raises_error(self):
with self.assertRaises(ValueError):
parse_config_line(self.invalid_line)
def tearDown(self):
# 清理工作(本例中不需要,但保留以展示结构)
pass
if __name__ == '__main__':
unittest.main()
运行结果:
....
----------------------------------------------------------------------
Ran 4 tests in 0.002s
OK
四个点代表四个测试全部通过。如果某个测试失败,会显示F并给出详细的失败信息。
3.6 常见陷阱与最佳实践
最后分享几个我在项目中踩过的坑:
- 测试之间共享状态:不要在
setUp里定义类变量,每个测试方法应该有自己的独立环境。我见过有人用self.counter在测试间传递数据,结果测试顺序一变就全崩了。 - 忽略
tearDown:如果setUp打开了文件、网络连接或硬件资源,tearDown一定要关闭。否则跑完100个测试,资源就泄漏光了。 - 测试太"聪明":测试代码应该简单直接,不要用复杂的逻辑。如果测试本身有bug,那它就是在帮倒忙。
- 只测正常路径:别忘了测试异常情况——空输入、非法参数、边界值。这些往往是bug藏身的地方。