3.2 调试与测试


3.2 调试与测试

本节摘要:写程序一半的时间在找错。本节讲两套功夫:调试器帮你找到当下的错——断点、单步、看变量,取代满屏 print 的盲搜;单元测试帮你守住未来的对——把"功能正确"写成一条命令就能验收的断言。你会先用调试器拆一个真实 bug,再给 Ledger 类配一个 unittest 测试类,体验绿灯亮起的踏实。

本节的能力清单

阅读完本节,你应当能够:

  1. 区分语法错误、运行时错误、逻辑错误,说出三者的发现时机
  2. 使用调试器设断点、单步执行、观察变量
  3. 写 unittest 测试类,掌握常用断言方法
  4. 给 Ledger 类补测试,跑通并理解"测试即验收标准"

三种错误,三种对策

语法错误解释器直接拒绝运行,行号都给你标好,最好对付。运行时错误执行到某行才炸,2.2 节的异常处理管的就是它。最难缠的是逻辑错误:程序不崩、跑得欢,就是结果不对——金额加错了、条件写反了、变量名敲混了。调试和测试这节功夫,主要就是对付逻辑错误。

图 3-2 从 print 盲搜到断点精查

图 3-2 从 print 盲搜到断点精查

断点调试实战:拆一个真实 bug

先造一个 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:把正确写成断言

调试解决"这一次的错",测试防止"以后的错"。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,跑测试看它怎么报警——亲眼看一次"测试兜底"。

易错点清单

  • setUp 里不重置状态:用例之间共享了脏数据,单独跑通过、一起跑就挂
  • 测试依赖真实数据文件:把正式账本当测试场,用例跑完数据被改——测试用独立文件名
  • 只测正常路径:负数金额、空输入、越界序号这些"坏情况"才是 bug 高发区
  • 断言写得宽松:assertIsNotNone 这种断言过不了几个 bug,该比就比到具体值

本节要点回顾

  • 逻辑错误最隐蔽,调试器给程序拍现场,比 print 盲搜高效
  • unittest 四件套:TestCase、setUp、test_ 方法、断言
  • 测试是验收标准:先写测试再实现,需求自动变成说明书
  • 测试用例之间必须隔离,setUp 全新开局
  • 绿灯给人改代码的底气,第 5 章换存储全靠它兜底

代码有人验收了。下一节给项目配上"时间机器"——Git 版本控制,每次改动都有据可查、有路可退。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U