第 39 课 - 单元测试基础(unittest 模块)
学习目标
完成本课学习后,你将能够:
- 理解什么是单元测试以及为什么它在软件开发中至关重要。
- 掌握使用 Python 内置的
unittest模块编写和组织测试用例。 - 熟练运用常用的断言方法来验证代码行为。
- 学会如何运行测试并理解测试结果的报告。
核心概念
想象一下,你刚组装好一个复杂的模型。在最终完成前,你会不会先检查每个小部件是否正常工作?单元测试就是对代码中的最小可测试单元(通常是一个函数或方法)进行检查,确保它按预期运行的过程。它就像给你的代码做"体检",能及早发现Bug,让你在修改代码时更有信心。
Python 提供了内置的 unittest 模块来帮助我们实现这个目标。它的工作方式可以类比为一个测试报告:
- 测试用例 (Test Case):一份具体的"体检项目",例如"测试
add函数能否正确计算 1 + 1"。在unittest中,一个测试用例就是一个类,里面包含多个以test_开头的方法。 - 测试套件 (Test Suite):一组相关的"体检项目",例如所有关于"数学运算"的测试用例。
- 测试运行器 (Test Runner):执行"体检项目"并生成报告的"医生"。
下面是一个最简单的测试类结构:
import unittest
# 我们要测试的代码(待测模块)
def add(a, b):
return a + b
# 测试用例类,必须继承自 unittest.TestCase
class TestAddFunction(unittest.TestCase):
# 测试方法,方法名必须以 test_ 开头
def test_add_positive_numbers(self):
# 使用断言方法来验证结果是否符合预期
result = add(1, 2)
self.assertEqual(result, 3) # 检查 1+2 是否等于 3
def test_add_negative_numbers(self):
self.assertEqual(add(-1, -2), -3)
# 这段代码通常在文件底部,用于直接运行此文件时执行测试
if __name__ == '__main__':
unittest.main()
核心断言方法
unittest.TestCase 类提供了许多用于验证结果的"断言"方法。常用的有:
assertEqual(a, b): 检查a == bassertTrue(x): 检查x为 TrueassertFalse(x): 检查x为 FalseassertIn(a, b): 检查a在b中assertRaises(Exception): 检查是否抛出了指定异常
代码示例
让我们来看一个更完整的例子,测试一个处理字符串的模块。
步骤 1: 创建待测模块 (string_utils.py)
# string_utils.py
def capitalize_words(s):
"""将字符串中每个单词的首字母大写"""
if not isinstance(s, str):
raise TypeError("Input must be a string")
if not s:
return s
return ' '.join(word.capitalize() for word in s.split())
步骤 2: 创建测试文件 (test_string_utils.py)
# test_string_utils.py
import unittest
from string_utils import capitalize_words # 导入要测试的函数
class TestCapitalizeWords(unittest.TestCase):
"""测试 capitalize_words 函数的测试用例类"""
def test_normal_sentence(self):
"""测试普通的句子"""
self.assertEqual(capitalize_words("hello world"), "Hello World")
def test_already_capitalized(self):
"""测试已经大写的句子,应保持不变"""
self.assertEqual(capitalize_words("Hello World"), "Hello World")
def test_empty_string(self):
"""测试空字符串输入"""
self.assertEqual(capitalize_words(""), "")
def test_single_word(self):
"""测试单个单词"""
self.assertEqual(capitalize_words("python"), "Python")
def test_input_type_error(self):
"""测试非字符串输入时是否抛出 TypeError"""
# 使用 assertRaises 上下文管理器来捕获预期的异常
with self.assertRaises(TypeError):
capitalize_words(123) # 传入数字,应引发 TypeError
def test_mixed_case_and_spaces(self):
"""测试包含多余空格和混合大小写的句子"""
self.assertEqual(capitalize_words(" hELLo wORLD "), " Hello World ")
# 当直接运行此文件时,执行测试
if __name__ == '__main__':
unittest.main(verbosity=2) # verbosity=2 会输出更详细的测试信息
步骤 3: 运行测试 在命令行中,进入测试文件所在目录,运行:
python test_string_utils.py
预期输出(摘要):
test_already_capitalized (__main__.TestCapitalizeWords) ... ok
test_empty_string (__main__.TestCapitalizeWords) ... ok
test_input_type_error (__main__.TestCapitalizeWords) ... ok
test_mixed_case_and_spaces (__main__.TestCapitalizeWords) ... ok
test_normal_sentence (__main__.TestCapitalizeWords) ... ok
test_single_word (__main__.TestCapitalizeWords) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
所有测试通过,输出 OK。
实践练习
请完成以下练习,巩固 unittest 的使用。
练习 1: 基础函数测试
编写一个函数 is_even(n),判断一个整数是否是偶数。然后为其编写单元测试,至少测试以下情况:
- 偶数(如 4, 0)
- 奇数(如 3, -5)
- 非整数输入(应抛出
TypeError) 预期:所有测试用例通过。
练习 2: 测试一个简单类
创建一个 BankAccount 类,它具有 __init__(self, balance) 和 withdraw(self, amount) 方法。withdraw 方法在余额不足时应抛出 ValueError。编写测试类,验证:
- 初始余额是否正确。
- 正常取款后余额是否正确更新。
- 取款金额大于余额时是否抛出
ValueError。 预期:能够正确模拟账户操作并验证边界条件。
练习 3: 组织测试套件(进阶)
在同一个测试文件中,为两个不同的函数(例如 add(a, b) 和 multiply(a, b))分别创建测试类 TestAdd 和 TestMultiply。然后,学习如何使用 unittest.TestLoader 和 unittest.TestSuite 手动组装一个只运行 TestMultiply 中所有测试的套件并执行。
提示:查阅 unittest 文档中关于 TestLoader().loadTestsFromTestCase() 的用法。
预期:成功构建并运行一个指定的测试套件,只看到乘法相关的测试结果。
常见错误
- 忘记导入:没有导入
unittest模块或要测试的函数/类。# 错误: NameError: name 'unittest' is not defined - 测试方法命名错误:测试方法没有以
test_开头,导致不会被识别和执行。def check_something(self): # 错误!应为 test_check_something ... - 错误地理解断言:使用了赋值
=而不是比较==,或者断言方法的参数顺序搞反(虽然assertEqual对参数顺序不敏感,但逻辑要清晰)。# 错误: 这是一个赋值,不是断言 self.assertEqual(result = 5) # 正确 self.assertEqual(result, 5) - 测试的不是行为而是实现:过度依赖具体实现细节(如检查某个内部变量的值),而不是公开接口的行为。这会导致测试非常脆弱,实现稍有改动就会失败。
- 忽略边缘情况:只测试了"正常路径",而忽略了空输入、极大/极小值、异常输入等边界情况。
小结
在本课中,我们学习了:
- 单元测试的价值:它是保证代码质量、支持安全重构和作为代码文档的重要手段。
unittest核心结构:通过继承unittest.TestCase创建测试类,在类中编写以test_开头的测试方法。- 断言是关键:使用
assertEqual,assertTrue,assertRaises等方法来明确声明你的预期结果。 - 测试组织:测试用例可以组织成类,进一步组织成测试套件。
- 运行测试:通过
if __name__ == '__main__': unittest.main()可以方便地直接运行测试文件。
掌握 unittest 是编写健壮 Python 程序的基石。下一课,我们将学习一个更流行、语法更简洁的第三方测试框架——pytest。