作者: andylin02
学习章节: 第9章 - 测试与调试
关键词: repr调试, unittest, TestCase, setUp, tearDown, Mock, pdb, 测试隔离, 依赖注入


第9章思维导图:7条建议全景

第9章 测试与调试 7条建议

调试基础

第75条: 用repr字符串输出调试信息

单元测试

第76条: 使用TestCase子类验证相关行为

第77条: setUp/tearDown隔离测试

模拟与依赖

第78条: 用unittest.mock模拟依赖

第79条: 封装依赖以方便模拟

交互调试

第80条: 考虑使用pdb交互式调试

高级追踪

第81条: 用tracemalloc追踪内存泄漏


第9章详细笔记:逐条精讲与源代码

本章讲授 Python 中保障代码质量的核心技能:测试与调试。你将学会编写整洁、隔离的单元测试,使用 Mock 对象剥离复杂依赖,以及运用交互式调试器与内存追踪工具快速定位问题。

第75条:使用 repr 字符串输出调试信息

核心repr() 返回对象的“官方”字符串表示,通常包含类型和定位信息,比直接 print 更适合调试。在自定义类中实现 __repr__ 可让日志和异常输出更加明确。

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x!r}, {self.y!r})"

v = Vector(1, 2)
print(repr(v))   # Vector(1, 2)
print(f"对象: {v!r}")  # 对象: Vector(1, 2)

# 调试时使用 repr 而不用 str
values = ["123", 456, Vector(3, 4)]
print(" ".join(repr(val) for val in values))
# '123' 456 Vector(3, 4)  ← 类型清晰

对比str() 面向用户,repr() 面向开发者。所有内置类型都有明确的 repr 实现。

关键收获:为每个类实现 __repr__ 是最小的调试友好投资。


第76条:使用 TestCase 子类验证相关行为

核心:Python 标准库 unittest 提供了 TestCase 基类,通过继承它即可编写结构化、可自动运行的测试。使用 assertEqualassertTrueassertRaises 等方法来验证行为。

import unittest

def add(a, b):
    return a + b

class TestAdd(unittest.TestCase):
    def test_add_positive(self):
        self.assertEqual(add(2, 3), 5)

    def test_add_negative(self):
        self.assertEqual(add(-1, -2), -3)

    def test_add_str(self):
        self.assertEqual(add("Hello, ", "World!"), "Hello, World!")

    def test_add_invalid(self):
        with self.assertRaises(TypeError):
            add(1, "2")

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

最佳实践:每个测试方法只验证一个行为,方法名以 test_ 开头,让框架自动发现。


第77条:使用 setUptearDown 隔离测试

核心:测试之间必须完全独立,否则一个测试的副作用会影响另一个。通过重写 setUp(每次测试前运行)和 tearDown(每次测试后运行)来准备和清理环境。

import unittest
import tempfile
import os

class TestFileIO(unittest.TestCase):
    def setUp(self):
        # 每个测试前创建临时文件
        self.temp_dir = tempfile.mkdtemp()
        self.temp_file = os.path.join(self.temp_dir, "test.txt")
        with open(self.temp_file, "w") as f:
            f.write("initial content")

    def tearDown(self):
        # 每个测试后清理
        os.remove(self.temp_file)
        os.rmdir(self.temp_dir)

    def test_read(self):
        with open(self.temp_file) as f:
            self.assertEqual(f.read(), "initial content")

    def test_write(self):
        with open(self.temp_file, "a") as f:
            f.write(" appended")
        with open(self.temp_file) as f:
            self.assertIn("appended", f.read())

模块级隔离:还可使用 setUpModule()tearDownModule() 在整个模块前后执行一次性操作。

关键收获setUp/tearDown 确保每个测试从相同的清洁起点开始。


第78条:使用 unittest.mock 模拟依赖

核心:当被测代码依赖外部服务(数据库、网络、时钟)时,使用 Mock 对象替换真实依赖,可让测试快速、确定且不产生副作用。unittest.mock.Mockpatch 是核心工具。

from unittest.mock import Mock, patch
import unittest
import datetime

def is_weekend():
    today = datetime.date.today()
    return today.weekday() >= 5  # 5=Saturday, 6=Sunday

class TestWeekend(unittest.TestCase):
    @patch('datetime.date')
    def test_is_weekend(self, mock_date):
        # 让 datetime.date.today() 返回固定日期
        mock_date.today.return_value = datetime.date(2023, 7, 15)  # 星期六
        self.assertTrue(is_weekend())

        mock_date.today.return_value = datetime.date(2023, 7, 17)  # 星期一
        self.assertFalse(is_weekend())

# 更通用的 Mock 用法
def send_email(service, to, subject):
    return service.send(to, subject)

class TestEmail(unittest.TestCase):
    def test_send_email(self):
        mock_service = Mock()
        mock_service.send.return_value = True
        result = send_email(mock_service, "a@b.com", "Hi")
        self.assertTrue(result)
        mock_service.send.assert_called_once_with("a@b.com", "Hi")

关键收获:Mock 让测试从外部不确定性中解放,可以验证“调用是否发生、参数是否正确”。


第79条:封装依赖以方便模拟

核心:直接导入外部模块并在函数内使用会令模拟变得困难。更好的做法是将外部依赖作为参数传入(依赖注入),或封装成专门的辅助类,这样测试时可以轻松替换。

# 不易测试:依赖全局模块
import datetime
def greet():
    if datetime.datetime.now().hour < 12:
        return "Good morning"
    return "Good afternoon"

# 易于测试:依赖注入
def greet(now=None):
    if now is None:
        now = datetime.datetime.now()
    if now.hour < 12:
        return "Good morning"
    return "Good afternoon"

# 测试时注入固定时间
from unittest.mock import Mock
mock_now = Mock(hour=9)
assert greet(mock_now) == "Good morning"

关键收获:好的设计让测试更简单——将难以控制的外部因素变为可传递的参数。


第80条:考虑使用 pdb 交互式调试

核心pdb 是 Python 内置的交互式调试器。在代码中插入 breakpoint()(Python 3.7+)即可进入 pdb 环境,检查变量、单步执行、修改变量,远比到处 print 高效。

def complex_calculation(x, y):
    result = x * y
    breakpoint()  # 程序在此停止,进入 pdb 控制台
    return result / (x - y)

# 运行后,在 pdb 中可以:
# p result         # 打印变量
# p x, y
# n                # 执行下一行
# c                # 继续运行
# q                # 退出

常用命令

  • n (next):下一行
  • s (step):步入函数
  • c (continue):继续执行
  • p <expr>:打印表达式
  • pp <expr>:美化打印
  • l:列出当前代码位置

关键收获breakpoint() 是调试的入口,让你像外科手术般检查运行时状态。


第81条:使用 tracemalloc 追踪内存使用和泄漏

核心:Python 3.4+ 提供的 tracemalloc 不仅能测量内存,还能追踪分配位置,是检测内存泄漏和异常大对象的利器。

import tracemalloc

tracemalloc.start()

def leak_func():
    global leak_list
    leak_list = [b'x' * 1024 for _ in range(1000)]

leak_func()

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('traceback')

for stat in top_stats[:3]:
    print(stat)
    for line in stat.traceback.format():
        print(line)
    print("---")

# 比较两个快照,找出增量
snapshot2 = tracemalloc.take_snapshot()
# 进一步分配后对比 snapshot 和 snapshot2 的差异

关键收获tracemalloc 提供确凿的内存分配证据,精准定位泄漏代码路径。


测试与调试工作流

内存

时间

编写新功能

编写单元测试

运行测试

测试通过?

考虑边缘情况,补充测试

使用repr/打印定位失败点

使用pdb进行交互调试

修复代码

提交代码

出现性能问题?

使用tracemalloc追踪

使用cProfile定位

优化


第9章总结:构建质量安全网

  1. 调试从 repr 开始:为所有类实现 __repr__,让输出说话。
  2. 测试需要结构化:使用 unittest.TestCase,每个方法测试一个行为。
  3. 隔离是根基:用 setUp/tearDown 创建每次独立的环境。
  4. 模拟是利器:用 Mockpatch 替换外部依赖,让测试飞起来。
  5. 设计为测试服务:将依赖参数化,便于替换。
  6. 调试利器 pdbbreakpoint() 让你在运行时拥有上帝视角。
  7. 追踪内存泄漏tracemalloc 提供精确的内存分配溯源。

掌握这些技能,你将能自信地迭代代码,快速修复缺陷,并维持系统的长期健康。


下一章预告:第10章 - 协作开发

当代码不仅为自己运行,而是融入团队时,就到了全书最后一章:协作开发

你将学到:

  • 如何组织模块与包,设计清晰的 API
  • 虚拟环境的配置与依赖管理
  • 编写有效的文档字符串
  • 为项目构建标准、可重现的开发环境

第10章将为你的 Python 技能树画上完整的句号,让你成为一个善于协作的专业开发者。


本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

更多推荐