Python入门学习教程:第 20 章 测试与调试

20.1 测试的重要性

在软件开发过程中,测试是保证代码质量的关键环节。通过测试可以发现代码中的错误、漏洞和不符合预期的行为,确保程序在各种情况下都能正确运行。

测试的主要目的:

  • 验证代码是否符合需求规格。
  • 发现代码中的逻辑错误和边界情况处理问题。
  • 确保代码在修改或扩展后仍然保持正确性(回归测试)。
  • 提高代码的可靠性和可维护性。

Python 提供了多种测试工具和框架,本章将重点介绍单元测试框架unittest(内置库)和pytest(第三方库),以及常用的调试方法。

20.2 单元测试

单元测试是对程序中最小的可测试单元(如函数、方法、类)进行的测试,验证每个单元是否能正确执行。

20.2.1 使用 unittest 框架

unittest是 Python 自带的单元测试框架,灵感来源于 Java 的 JUnit,提供了测试用例、测试套件、测试夹具和断言等功能。

1. 基本用法

使用unittest编写测试的步骤:

  1. 导入unittest模块。
  1. 创建一个继承自unittest.TestCase的测试类。
  1. 在测试类中定义以test_开头的测试方法,每个方法对应一个测试用例。
  1. 使用unittest提供的断言方法验证结果是否符合预期。
  1. 运行测试用例。

示例:对一个简单的数学工具函数进行单元测试

# 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 为例:

  1. 在代码左侧点击设置断点(显示为红色圆点)。
  1. 点击 “运行和调试” 按钮,选择 “Python 文件” 配置。
  1. 程序会在断点处暂停,使用调试工具栏进行单步执行、继续等操作。
  1. 在 “变量” 面板中查看当前作用域的变量值。

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

  1. 运行测试并收集覆盖率数据:
coverage run --source=math_utils -m pytest test_math_utils.py  # --source指定要测量的模块
  1. 生成覆盖率报告:
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 的性能优化方法,帮助你编写更高效的代码。

相关文章

cython如何调用C语言的函数?_c 中如何调用python

在 Cython 中调用 C 语言函数主要通过以下几种方式实现:1. 使用 cdef extern 声明外部 C 函数基本语法cdef extern from "头文件.h":返回类型...

C/C++函数调用的奥秘_c++函数调用原理

在C/C++编程的世界里,函数调用是程序运行的核心机制之一。然而,许多程序员在日常开发中,往往只关注代码的逻辑,而忽略了函数调用背后的底层细节。今天,就让我们一起深入探索C/C++函数调用的全过程,从...

C++成员函数如何工作?this指针、name mangling 成员函数指针解析

0.引言 在C++面向对象编程中,成员函数是对象行为的核心载体。我们每天都在使用成员函数,但却很少深入思考其底层的实现机制:为什么成员函数可以直接访问成员变量?编译器如何区分不同类的同名函数?静态成员...

C语言入门:学生成绩管理程序的完善(1):用文件保存数据

这是C语言入门的第27篇文章。今天讲学生成绩管理程序的完善:怎样利用文件来保存数据。还是昨天的问题:我怎么知道一个文件的内容是什么?我怎么知道比如一行有多少个数,多少个数以后是换行?这是因为文件是我们...

C语言应用笔记:常用的printf打印输出不同类型数据

我叫程序员阿虾, 在终端前摸过太多凌晨, 熟悉printf这一行字带来的安心与危险。今天想跟你聊聊我踩过的坑, 和一些别人不常说的细节, 用第一人称把经验交给你, 有点唠叨, 希望你少走弯路。为什么要...

C语言应用笔记:简单的最大最小值比较

使用宏定义实现泛型比较函数,用于求取两个值的最大值和最小值。核心宏定义解析#define MAX(x, y) ((x) > (y) ? (x) : (y)) // 返回两个值中较大的一个 #de...