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