Files

7.2 KiB
Raw Permalink Blame History

sources, brief
sources brief
summaries/08_Testing_debugging__00_Overview.md
summaries/Contents.md
summaries/01_Testing.md
单元测试是验证函数、类或模块等最小代码单元行为是否符合预期的测试方法。

单元测试

单元测试是一种软件测试方法,用于验证程序中较小、相对独立的代码单元是否按照预期工作。这些单元可以是函数、方法、类或模块。在 Python 中,单元测试尤其重要,因为 Python 是动态语言,很多错误不会在编译阶段暴露,只能通过运行代码和测试行为来发现。相关背景见 summaries/01_Testing。

为什么需要单元测试

summaries/01_Testing 强调:Python 没有编译器替开发者提前发现大量错误,因此发现 bug 的主要方式是运行代码。单元测试通过系统化地运行一组小规模测试,帮助开发者确认代码行为是否正确。

单元测试的主要价值包括:

  • 尽早发现函数、类或模块中的错误。
  • 验证代码在典型输入下是否返回正确结果。
  • 验证代码在错误输入下是否抛出预期异常。
  • 防止后续修改破坏已有功能。
  • 为代码行为提供可执行的文档。

这与 软件测试 的整体目标一致,但单元测试更关注最小粒度代码单元。

Python 中的单元测试

Python 标准库提供了 unittest 模块,用于编写结构化单元测试。典型做法是将业务代码和测试代码分开,例如:

# simple.py

def add(x, y):
    return x + y

对应测试文件可以写成:

# test_simple.py

import simple
import unittest

class TestAdd(unittest.TestCase):
    def test_simple(self):
        r = simple.add(2, 2)
        self.assertEqual(r, 4)

    def test_str(self):
        r = simple.add('hello', 'world')
        self.assertEqual(r, 'helloworld')

这里有几个关键规则:

  • 测试类通常继承自 unittest.TestCase。
  • 测试方法名称必须以 test 开头,测试运行器才会识别。
  • 每个测试方法验证一个具体行为或场景。
  • 使用断言方法表达期望结果。

相关概念:Python unittest、测试用例、测试运行器。

测试断言

单元测试的核心是断言:声明某个条件应该成立。如果断言失败,测试就失败。

在 unittest 中,常见断言包括:

self.assertTrue(expr)                  # 判断表达式为 True
self.assertEqual(x, y)                 # 判断 x == y
self.assertNotEqual(x, y)              # 判断 x != y
self.assertAlmostEqual(x, y, places)   # 判断数值近似相等
self.assertRaises(exc, callable, ...)  # 判断调用会抛出指定异常

例如,测试 add(2, 2) 是否等于 4:

self.assertEqual(simple.add(2, 2), 4)

如果实际结果不是 4,测试框架会报告失败原因。

相关概念:concepts/断言、测试断言。

运行单元测试

使用 unittest 时,测试文件通常包含:

if __name__ == '__main__':
    unittest.main()

然后可以直接运行:

python3 test_simple.py

运行后,unittest 会报告:

  • 执行了多少个测试。
  • 哪些测试通过。
  • 哪些测试失败。
  • 失败位置和断言错误信息。

例如,如果测试期望 5,但实际结果是 4,会出现类似:

AssertionError: 4 != 5

这类失败报告能帮助开发者快速定位错误行为。

测试异常情况

单元测试不仅要测试正常输入,也应该测试错误输入。对于应当抛出异常的场景,可以使用 assertRaises。

在 summaries/01_Testing 的练习中,Stock 类的 shares 属性应该只能设置为整数。如果将其设置为字符串,应抛出 TypeError:

def test_bad_shares(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(TypeError):
        s.shares = '100'

这种测试能确认代码不仅在正确使用时有效,也能在错误使用时按预期失败。

相关概念:异常测试、类型检查。

面向对象代码的单元测试

单元测试也常用于测试类的行为。summaries/01_Testing 中的练习要求为 Stock 类编写测试,覆盖以下内容:

  1. 对象是否能正确创建。
  2. 属性值是否正确保存。
  3. 计算属性是否返回正确结果。
  4. 方法调用是否正确改变对象状态。
  5. 属性类型约束是否生效。

示例:

class TestStock(unittest.TestCase):
    def test_create(self):
        s = stock.Stock('GOOG', 100, 490.1)
        self.assertEqual(s.name, 'GOOG')
        self.assertEqual(s.shares, 100)
        self.assertEqual(s.price, 490.1)

进一步可以测试:

  • s.cost 是否返回 49010.0。
  • s.sell() 是否能正确减少 s.shares。
  • s.shares = '100' 是否抛出 TypeError。

相关概念:面向对象测试、属性测试。

与内联测试的区别

简单断言也可以直接写在模块中:

def add(x, y):
    return x + y

assert add(2, 2) == 4

这种方式可以作为基本的“冒烟测试”:如果模块导入时就失败,说明代码存在明显问题。

但内联测试不适合大型或系统化测试,因为:

  • 测试代码和业务代码混在一起。
  • 难以组织大量测试场景。
  • 难以生成完整测试报告。
  • 不适合复杂项目中的自动化测试流程。

因此,正式项目通常使用 unittest 或 pytest 等测试框架来组织单元测试。

相关概念:冒烟测试、测试组织。

unittest 与 pytest

unittest 是 Python 标准库的一部分,优点是无需额外安装,适合任何 Python 环境。但它的写法相对冗长。

pytest 是常用第三方测试工具,语法更简洁。例如:

import simple

def test_simple():
    assert simple.add(2, 2) == 4

def test_str():
    assert simple.add('hello', 'world') == 'helloworld'

运行方式通常是:

python -m pytest

pytest 会自动发现测试并执行。它也支持直接使用 Python 的 assert 语句,使测试代码更接近普通函数。

相关概念:concepts/pytest、测试发现、Python测试工具。

良好单元测试的特征

一个好的单元测试通常具备以下特点:

  • 范围小:只验证一个函数、方法或行为。
  • 目标明确:清楚表达期望结果。
  • 可重复运行:每次运行结果应一致。
  • 失败信息有意义:失败时能帮助定位问题。
  • 覆盖正常和异常场景:不仅测试成功路径,也测试错误路径。
  • 与业务代码分离:通常放在独立测试文件中。

小结

单元测试是 Python 开发中的基础实践。它通过运行小规模、可重复的测试来验证代码行为,弥补动态语言缺少编译期检查的不足。summaries/01_Testing 展示了从简单断言到 unittest,再到 pytest 的测试方式,并通过 Stock 类练习说明如何测试对象创建、属性计算、方法行为和异常情况。

相关页面:summaries/01_Testing、软件测试、concepts/断言、Python unittest、concepts/pytest、异常测试。

See also: summaries/Contents

See also: summaries/08_Testing_debugging__00_Overview