本节摘要:写程序一半的时间在找错。本节讲两套功夫:调试器帮你找到当下的错——断点、单步、看变量,取代满屏 print 的盲搜;单元测试帮你守住未来的对——把"功能正确"写成一条命令就能验收的断言。你会先用调试器拆一个真实 bug,再给 Ledger 类配一个 unittest 测试类,体验绿灯亮起的踏实。
阅读完本节,你应当能够:
语法错误解释器直接拒绝运行,行号都给你标好,最好对付。运行时错误执行到某行才炸,2.2 节的异常处理管的就是它。最难缠的是逻辑错误:程序不崩、跑得欢,就是结果不对——金额加错了、条件写反了、变量名敲混了。调试和测试这节功夫,主要就是对付逻辑错误。

先造一个 bug:月度统计把月份前缀比较写反了。
def month_total(records, month): total = 0 for r in records: if month.startswith(r["date"]): # bug:方向反了 total += r["amount"] return total records = [ {"item": "早饭", "amount": 12.5, "date": "2026-09-01"}, {"item": "地铁", "amount": 4.0, "date": "2026-08-30"}, ] print(month_total(records, "2026-09")) # 期望 12.5,实际 0 —— 逻辑错误
程序不报错,结果错。调试三步:在 if 行设断点;以调试模式运行,程序在断点处冻结;观察面板里 month 与 r["date"] 的值——前者是 "2026-09",后者是 "2026-09-01",一眼看出 startswith 的方向写反了,应该是 r["date"].startswith(month)。改掉,重跑,输出 12.5。调试器的本质是给程序拍下"案发现场",变量值全摊在桌面上,比脑内模拟可靠十倍。编辑器左侧行号旁点一下就是断点;临时无编辑器时,标准库的 breakpoint() 一行也能冻结现场。
调试解决"这一次的错",测试防止"以后的错"。unittest 是标准库自带的测试框架:
import unittest from datetime import date class TestLedger(unittest.TestCase): def setUp(self): """每个测试方法运行前都执行:准备全新账本。""" self.ledger = Ledger(data_file="test_ledger.json") def test_add_record(self): self.ledger.add_record("早饭", 12.5) self.assertEqual(self.ledger.count, 1) # 断言:条数应为 1 def test_reject_negative(self): with self.assertRaises(ValueError): # 断言:应抛异常 self.ledger.add_record("退款", -5) def test_month_total(self): self.ledger.add_record("早饭", 12.5) self.assertAlmostEqual(self.ledger.month_total("2026-09"), 12.5)
组织方式与类如出一辙:测试类继承 TestCase;setUp 负责每个用例的"全新开局",保证用例之间互不污染;每个 test_ 开头的方法是一个独立用例。常用断言就五个:assertEqual 相等、assertTrue 为真、assertRaises 该抛异常、assertAlmostEqual 浮点近似相等、assertIn 包含。跑测试一条命令:
python -m unittest discover # 输出末尾:Ran 3 tests in 0.004s OK
看到 OK 就是一盏绿灯。测试的价值在改代码时兑现:将来把账目存储从 JSON 换成数据库(第 5 章),跑一遍测试,绿灯意味着行为没变——你敢大改的底气来自这里。
试试 TDD 的味道:先写一个还不存在的功能的测试。
def test_big_records(self): self.ledger.add_record("手机", 4999) self.ledger.add_record("早饭", 12.5) big = self.ledger.records_above(100) self.assertEqual(len(big), 1) self.assertEqual(big[0]["item"], "手机")
运行,红灯——records_above 不存在。去 Ledger 类里实现它,再跑,绿灯。先写测试再写实现,测试就成了需求的说明书。变式一:给 delete_by_index 补两个用例:正常删除、越界删除该抛异常。变式二:故意在实现里引入一个 bug,跑测试看它怎么报警——亲眼看一次"测试兜底"。
代码有人验收了。下一节给项目配上"时间机器"——Git 版本控制,每次改动都有据可查、有路可退。