7.7 KiB
doc_type, full_text
| doc_type | full_text |
|---|---|
| short | sources/01_Testing.md |
01_Testing 总结
本文介绍 Python 中测试的基本思想与实践方式,强调动态语言缺少编译期检查,因此需要通过运行代码和系统化测试来发现问题。核心内容包括 assert 断言、契约式编程、内联冒烟测试、标准库 unittest、第三方工具 pytest,以及围绕 Stock 类编写单元测试的练习。
测试的重要性
Python 的动态特性使测试对大多数应用至关重要:
- 没有编译器帮助提前发现大量类型或接口错误。
- 发现 bug 的主要方式是运行代码。
- 测试需要尽可能覆盖程序功能,验证代码行为符合预期。
文章用一句话概括态度:测试很棒,调试很糟。也就是说,主动编写测试比事后依赖调试更可靠。
相关概念:concepts/软件测试、Python动态类型
assert 断言
assert 是程序内部检查机制。如果表达式不为真,就会抛出 AssertionError。
assert <expression> [, 'Diagnostic message']
示例:
assert isinstance(10, int), 'Expected int'
assert 适合用于检查程序内部不变量和假设,不适合用于校验用户输入。例如,不应依赖 assert 检查 Web 表单提交的数据。
相关概念:concepts/断言、程序不变量
契约式编程
契约式编程,也称 Design by Contract,是大量使用断言来定义组件接口规格的一种设计方法。
例如,可以在函数入口检查参数类型:
def add(x, y):
assert isinstance(x, int), 'Expected int'
assert isinstance(y, int), 'Expected int'
return x + y
这样可以尽早发现调用者传入了不符合预期的参数:
>>> add('2', '3')
AssertionError: Expected int
这种方式将函数对调用者的要求明确写进代码中,有助于定位接口使用错误。
相关概念:契约式编程、接口设计、类型检查
内联测试
断言也可以用作简单测试:
def add(x, y):
return x + y
assert add(2, 2) == 4
这种测试直接放在模块代码中。它的优点是:如果代码明显损坏,导入模块时就会失败。
不过,内联断言不适合做全面测试,更适合作为基础的“冒烟测试”:确认函数在最简单的例子上是否能正常工作。
相关概念:冒烟测试、测试组织
unittest 模块
Python 标准库提供 unittest 模块,用于编写结构化单元测试。
假设有业务代码:
# simple.py
def add(x, y):
return x + y
可以创建单独的测试文件:
# test_simple.py
import simple
import unittest
测试类必须继承自 unittest.TestCase:
class TestAdd(unittest.TestCase):
...
测试方法必须以 test 开头,否则不会被测试运行器自动识别:
class TestAdd(unittest.TestCase):
def test_simple(self):
r = simple.add(2, 2)
self.assertEqual(r, 5)
def test_str(self):
r = simple.add('hello', 'world')
self.assertEqual(r, 'helloworld')
相关概念:concepts/单元测试、Python unittest、测试用例
unittest 常用断言
unittest.TestCase 提供多种断言方法,用于表达不同测试期望:
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, ...) # 判断调用会抛出指定异常
这些只是部分方法,unittest 还提供了更多断言、测试运行器和结果收集功能。
相关概念:测试断言、异常测试
运行 unittest
测试文件通常包含如下入口:
if __name__ == '__main__':
unittest.main()
然后可以直接运行测试文件:
python3 test_simple.py
如果测试失败,unittest 会报告失败的测试方法、调用栈和断言失败原因。例如 self.assertEqual(r, 5) 在实际结果为 4 时会显示:
AssertionError: 4 != 5
测试输出会统计运行数量、耗时以及失败情况。
相关概念:测试运行器、测试失败报告
第三方测试工具:pytest
虽然 unittest 是标准库的一部分,优点是随 Python 可用,但许多程序员认为它较为冗长。文章介绍了常见替代方案 pytest。
使用 pytest 时,测试文件可以更简洁:
# test_simple.py
import simple
def test_simple():
assert simple.add(2, 2) == 4
def test_str():
assert simple.add('hello', 'world') == 'helloworld'
运行方式:
python -m pytest
pytest 会自动发现测试并执行。文章指出,pytest 功能远不止这个例子,但入门通常很容易。
相关概念:concepts/pytest、测试发现、Python测试工具
练习:为 Stock 类编写单元测试
练习要求为之前实现的 Stock 类编写测试文件 test_stock.py。该类来自前面关于 typed-properties 的练习。
起始测试用于验证实例创建:
import unittest
import stock
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)
if __name__ == '__main__':
unittest.main()
运行成功时输出类似:
.
----------------------------------------------------------------------
Ran 1 tests in 0.000s
OK
随后需要补充测试:
s.cost属性是否返回正确值49010.0。s.sell()方法是否能正确减少s.shares。s.shares是否不能被设置为非整数值。
测试异常可以使用上下文管理器形式的 assertRaises:
def test_bad_shares(self):
s = stock.Stock('GOOG', 100, 490.1)
with self.assertRaises(TypeError):
s.shares = '100'
这个练习将单元测试应用到对象创建、属性计算、方法副作用和类型约束验证等场景。
相关概念:面向对象测试、属性测试、异常测试
核心要点
- Python 动态语言特性使测试尤其重要。
assert用于程序内部检查和不变量验证,不应用于用户输入验证。- 契约式编程通过断言明确接口前置条件。
- 内联断言适合做简单冒烟测试,但不适合完整测试体系。
unittest是 Python 标准单元测试框架,基于TestCase、test_方法和断言方法。unittest.main()可用于从脚本运行测试。pytest提供更简洁的测试编写和自动发现机制。- 编写测试应覆盖对象初始化、属性、方法行为和异常情况。
可延伸的概念页
- 软件测试:测试在软件开发中的作用与层次。
- concepts/单元测试:围绕函数、类和模块的最小粒度验证。
- concepts/断言:运行时假设检查与测试表达方式。
- 契约式编程:通过前置条件、后置条件和不变量设计接口。
- Python unittest:Python 标准测试框架的结构与用法。
- concepts/pytest:Python 第三方测试框架及其测试发现机制。
- 异常测试:验证代码在错误输入下是否抛出预期异常。