Python入门学习教程:第 20 章 测试与调试
20.1 测试的重要性
在软件开发过程中,测试是保证代码质量的关键环节。通过测试可以发现代码中的错误、漏洞和不符合预期的行为,确保程序在各种情况下都能正确运行。
测试的主要目的:
- 验证代码是否符合需求规格。
- 发现代码中的逻辑错误和边界情况处理问题。
- 确保代码在修改或扩展后仍然保持正确性(回归测试)。
- 提高代码的可靠性和可维护性。
Python 提供了多种测试工具和框架,本章将重点介绍单元测试框架unittest(内置库)和pytest(第三方库),以及常用的调试方法。
20.2 单元测试
单元测试是对程序中最小的可测试单元(如函数、方法、类)进行的测试,验证每个单元是否能正确执行。
20.2.1 使用 unittest 框架
unittest是 Python 自带的单元测试框架,灵感来源于 Java 的 JUnit,提供了测试用例、测试套件、测试夹具和断言等功能。
1. 基本用法
使用unittest编写测试的步骤:
- 导入unittest模块。
- 创建一个继承自unittest.TestCase的测试类。
- 在测试类中定义以test_开头的测试方法,每个方法对应一个测试用例。
- 使用unittest提供的断言方法验证结果是否符合预期。
- 运行测试用例。
示例:对一个简单的数学工具函数进行单元测试
# math_utils.py
def add(a, b):
"""返回两个数的和"""
return a + b
def multiply(a, b):
"""返回两个数的乘积"""
return a * b
def divide(a, b):
"""返回两个数的商(b不能为0)"""
if b == 0:
raise ValueError("除数不能为0")
return a / b
# test_math_utils.py
import unittest
from math_utils import add, multiply, divide
class TestMathUtils(unittest.TestCase):
"""测试数学工具函数的测试类"""
def test_add(self):
"""测试add函数"""
self.assertEqual(add(2, 3), 5) # 断言结果等于预期值
self.assertEqual(add(-1, 1), 0)
self.assertEqual(add(0, 0), 0)
def test_multiply(self):
"""测试multiply函数"""
self.assertEqual(multiply(2, 3), 6)
self.assertEqual(multiply(-1, 1), -1)
self.assertEqual(multiply(0, 5), 0)
def test_divide(self):
"""测试divide函数"""
self.assertEqual(divide(6, 3), 2)
self.assertEqual(divide(5, 2), 2.5)
self.assertEqual(divide(-4, 2), -2)
# 测试异常情况(除数为0时应抛出ValueError)
with self.assertRaises(ValueError):
divide(1, 0)
if __name__ == "__main__":
unittest.main() # 运行测试
- assertEqual(a, b):断言a等于b。
- assertRaises(exception):断言代码块会抛出指定的异常。
- 其他常用断言方法:assertNotEqual()、assertTrue()、assertFalse()、assertIsNone()、assertIn()等。
运行测试脚本,输出结果类似:
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
每个.表示一个测试用例通过,如果测试失败会显示错误信息。
2. 测试夹具(Test Fixture)
测试夹具用于在测试前后执行一些准备和清理工作,如初始化资源、创建临时文件、连接数据库等。
unittest提供了两个特殊方法:
- setUp():在每个测试方法执行前调用,用于准备测试环境。
- tearDown():在每个测试方法执行后调用,用于清理测试环境。
示例:使用测试夹具
import unittest
from math_utils import add
class TestAddWithFixture(unittest.TestCase):
def setUp(self):
"""每个测试方法执行前调用"""
print("\n准备测试数据...")
self.data = [(1, 2, 3), (0, 0, 0), (-1, 1, 0)]
def tearDown(self):
"""每个测试方法执行后调用"""
print("清理测试数据...")
self.data = None
def test_add(self):
"""使用测试数据测试add函数"""
for a, b, expected in self.data:
with self.subTest(a=a, b=b): # 子测试,单独报告每个数据的测试结果
self.assertEqual(add(a, b), expected)
if __name__ == "__main__":
unittest.main()
20.2.2 使用 pytest 框架
pytest是一个功能更强大、使用更简洁的第三方测试框架,支持unittest风格的测试用例,也支持更简洁的函数式测试。
1. 安装 pytest
pip install pytest
2. 基本用法
pytest的测试用例可以是函数或类,无需继承特定的基类,测试函数以test_开头,测试类以Test开头且不含__init__方法。
示例:使用 pytest 测试数学工具函数
# test_math_utils_pytest.py
from math_utils import add, multiply, divide
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_multiply():
assert multiply(2, 3) == 6
assert multiply(-1, 1) == -1
assert multiply(0, 5) == 0
def test_divide():
assert divide(6, 3) == 2
assert divide(5, 2) == 2.5
assert divide(-4, 2) == -2
# 测试异常
with pytest.raises(ValueError) as excinfo:
divide(1, 0)
assert "除数不能为0" in str(excinfo.value)
在命令行中运行测试:
pytest test_math_utils_pytest.py -v # -v显示详细信息
pytest的优势:
- 语法简洁,无需继承类,使用普通assert语句即可。
- 自动发现测试用例(搜索以test_开头的函数和类)。
- 丰富的插件生态(如测试覆盖率、HTML 报告等)。
20.3 调试技巧
调试是定位并修复代码中错误的过程。当程序运行结果不符合预期或抛出异常时,需要通过调试找出问题所在。
20.3.1 使用 print 语句调试
最简单的调试方法是在代码中插入print语句,输出变量的值、函数的执行流程等信息,帮助判断程序的执行状态。
示例:
def calculate_average(numbers):
print(f"输入数据:{numbers}") # 输出输入数据
total = 0
count = 0
for num in numbers:
total += num
count += 1
print(f"累加后:total={total}, count={count}") # 输出中间结果
if count == 0:
return 0
average = total / count
print(f"计算得到的平均值:{average}") # 输出结果
return average
calculate_average([1, 2, 3, 4])
20.3.2 使用 pdb 调试器
Python 的标准库pdb提供了一个交互式调试器,可以在程序运行过程中暂停执行、查看变量、单步执行等。
常用的 pdb 命令:
- break(b):设置断点。
- continue(c):继续执行到下一个断点。
- step(s):单步执行,进入函数内部。
- next(n):单步执行,不进入函数内部。
- print(p):打印变量的值。
- list(l):显示当前执行的代码。
- quit(q):退出调试器。
示例:使用 pdb 调试
import pdb
def calculate_average(numbers):
pdb.set_trace() # 设置断点,程序执行到此处会暂停
total = 0
count = 0
for num in numbers:
total += num
count += 1
if count == 0:
return 0
return total / count
calculate_average([1, 2, 3, 4])
运行程序后,会进入 pdb 调试环境,输入上述命令进行调试。
20.3.3 使用 IDE 调试工具
大多数集成开发环境(IDE)如 PyCharm、VS Code 等都提供了图形化的调试工具,使用更方便,支持点击设置断点、查看变量面板、单步执行等功能。
以 VS Code 为例:
- 在代码左侧点击设置断点(显示为红色圆点)。
- 点击 “运行和调试” 按钮,选择 “Python 文件” 配置。
- 程序会在断点处暂停,使用调试工具栏进行单步执行、继续等操作。
- 在 “变量” 面板中查看当前作用域的变量值。
20.4 错误处理与日志
良好的错误处理和日志记录可以帮助快速定位问题,尤其是在大型应用程序中。
20.4.1 异常处理
使用try-except语句捕获和处理异常,避免程序崩溃,并提供有用的错误信息。
示例:
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
print(f"错误:除数不能为0 - {e}")
return None
except TypeError as e:
print(f"错误:参数必须是数字 - {e}")
return None
print(safe_divide(5, 0)) # 输出错误信息和None
print(safe_divide(5, "2")) # 输出错误信息和None
20.4.2 日志记录
logging模块提供了比print更灵活的日志记录功能,可以设置日志级别、输出到文件或控制台、格式化日志信息等。
示例:使用 logging 记录日志
import logging
# 配置日志:级别为DEBUG,格式包含时间、级别和消息
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(message)s",
filename="app.log" # 日志输出到文件(不指定则输出到控制台)
)
def calculate_average(numbers):
logging.debug(f"开始计算平均值,输入数据:{numbers}")
try:
total = sum(numbers)
count = len(numbers)
if count == 0:
logging.warning("输入数据为空,返回0")
return 0
average = total / count
logging.info(f"平均值计算完成:{average}")
return average
except TypeError as e:
logging.error(f"计算失败:{e}", exc_info=True) # exc_info=True记录异常堆栈
return None
calculate_average([1, 2, 3, 4])
calculate_average([])
calculate_average([1, "2", 3])
日志级别(从低到高):
- DEBUG:详细的调试信息。
- INFO:程序正常运行的信息。
- WARNING:潜在的问题,但不影响程序运行。
- ERROR:错误导致功能无法执行。
- CRITICAL:严重错误,可能导致程序终止。
20.5 测试覆盖率
测试覆盖率是衡量测试用例覆盖代码比例的指标,用于评估测试的充分性。coverage.py是 Python 中常用的测试覆盖率工具。
20.5.1 安装 coverage.py
pip install coverage
20.5.2 使用 coverage.py
- 运行测试并收集覆盖率数据:
coverage run --source=math_utils -m pytest test_math_utils.py # --source指定要测量的模块
- 生成覆盖率报告:
coverage report # 文本报告
coverage html # 生成HTML报告(在htmlcov目录中)
文本报告示例:
Name Stmts Miss Cover
---------------------------------
math_utils.py 10 0 100%
---------------------------------
TOTAL 10 0 100%
- Stmts:语句总数。
- Miss:未被测试覆盖的语句数。
- Cover:覆盖率百分比。
20.6 小结
本章介绍了 Python 的测试与调试方法,包括:
- 单元测试:使用unittest和pytest框架编写测试用例,验证函数和方法的正确性。
- 测试夹具:通过setUp()和tearDown()方法准备和清理测试环境。
- 调试技巧:使用print语句、pdb调试器和 IDE 调试工具定位错误。
- 错误处理与日志:使用try-except处理异常,logging模块记录程序运行信息。
- 测试覆盖率:使用coverage.py评估测试的充分性。
测试和调试是软件开发不可或缺的环节,良好的测试习惯可以显著提高代码质量和可维护性,减少后期维护成本。在实际开发中,应尽量编写全面的测试用例,并结合调试工具快速解决问题。
下一章将介绍 Python 的性能优化方法,帮助你编写更高效的代码。