39·模块九:测试与调试进阶

单元测试基础(unittest 模块)

testingunittest

第 39 课 - 单元测试基础(unittest 模块)

学习目标

完成本课学习后,你将能够:

  1. 理解什么是单元测试以及为什么它在软件开发中至关重要。
  2. 掌握使用 Python 内置的 unittest 模块编写和组织测试用例。
  3. 熟练运用常用的断言方法来验证代码行为。
  4. 学会如何运行测试并理解测试结果的报告。

核心概念

想象一下,你刚组装好一个复杂的模型。在最终完成前,你会不会先检查每个小部件是否正常工作?单元测试就是对代码中的最小可测试单元(通常是一个函数或方法)进行检查,确保它按预期运行的过程。它就像给你的代码做"体检",能及早发现Bug,让你在修改代码时更有信心。

Python 提供了内置的 unittest 模块来帮助我们实现这个目标。它的工作方式可以类比为一个测试报告

  1. 测试用例 (Test Case):一份具体的"体检项目",例如"测试 add 函数能否正确计算 1 + 1"。在 unittest 中,一个测试用例就是一个类,里面包含多个以 test_ 开头的方法。
  2. 测试套件 (Test Suite):一组相关的"体检项目",例如所有关于"数学运算"的测试用例。
  3. 测试运行器 (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 == b
  • assertTrue(x): 检查 x 为 True
  • assertFalse(x): 检查 x 为 False
  • assertIn(a, b): 检查 ab
  • 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),判断一个整数是否是偶数。然后为其编写单元测试,至少测试以下情况:

  1. 偶数(如 4, 0)
  2. 奇数(如 3, -5)
  3. 非整数输入(应抛出 TypeError预期:所有测试用例通过。

练习 2: 测试一个简单类

创建一个 BankAccount 类,它具有 __init__(self, balance)withdraw(self, amount) 方法。withdraw 方法在余额不足时应抛出 ValueError。编写测试类,验证:

  1. 初始余额是否正确。
  2. 正常取款后余额是否正确更新。
  3. 取款金额大于余额时是否抛出 ValueError预期:能够正确模拟账户操作并验证边界条件。

练习 3: 组织测试套件(进阶)

在同一个测试文件中,为两个不同的函数(例如 add(a, b)multiply(a, b))分别创建测试类 TestAddTestMultiply。然后,学习如何使用 unittest.TestLoaderunittest.TestSuite 手动组装一个只运行 TestMultiply 中所有测试的套件并执行。 提示:查阅 unittest 文档中关于 TestLoader().loadTestsFromTestCase() 的用法。 预期:成功构建并运行一个指定的测试套件,只看到乘法相关的测试结果。

常见错误

  1. 忘记导入:没有导入 unittest 模块或要测试的函数/类。
    # 错误: NameError: name 'unittest' is not defined
    
  2. 测试方法命名错误:测试方法没有以 test_ 开头,导致不会被识别和执行。
    def check_something(self): # 错误!应为 test_check_something
        ...
    
  3. 错误地理解断言:使用了赋值 = 而不是比较 ==,或者断言方法的参数顺序搞反(虽然 assertEqual 对参数顺序不敏感,但逻辑要清晰)。
    # 错误: 这是一个赋值,不是断言
    self.assertEqual(result = 5)
    # 正确
    self.assertEqual(result, 5)
    
  4. 测试的不是行为而是实现:过度依赖具体实现细节(如检查某个内部变量的值),而不是公开接口的行为。这会导致测试非常脆弱,实现稍有改动就会失败。
  5. 忽略边缘情况:只测试了"正常路径",而忽略了空输入、极大/极小值、异常输入等边界情况。

小结

在本课中,我们学习了:

  • 单元测试的价值:它是保证代码质量、支持安全重构和作为代码文档的重要手段。
  • unittest 核心结构:通过继承 unittest.TestCase 创建测试类,在类中编写以 test_ 开头的测试方法。
  • 断言是关键:使用 assertEqual, assertTrue, assertRaises 等方法来明确声明你的预期结果。
  • 测试组织:测试用例可以组织成类,进一步组织成测试套件。
  • 运行测试:通过 if __name__ == '__main__': unittest.main() 可以方便地直接运行测试文件。

掌握 unittest 是编写健壮 Python 程序的基石。下一课,我们将学习一个更流行、语法更简洁的第三方测试框架——pytest

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「使用 pytest 进行测试」 以巩固所学知识。