调试与测试:Bug查得快,写得稳
一、问题背景一个空字符串坑了我3天去年有件让我印象特别深刻的事。写了一个MES数据导入脚本跑了半年没问题。突然有一天某批次的厚度平均值算出来是负数。查了3天最后发现**有一行的厚度值被MES导成了空字符串被我转成了0。**更坑的是上个月也有一批数据少一个值但因为数据量大一个0对平均值影响小没发现。这次恰好样本少一个0直接拉偏了整个平均值。**为什么没发现** 因为写代码的时候只想了正常情况没想数据不完美的情况。**教训**代码能跑 ≠ 代码没问题。Bug藏得越深排查越痛苦。**学完这一篇你能做到**1. 用 print breakpoint 快速找Bug2. 用 assert 在代码里自动检查数据3. 用 unittest 写简单的测试上线前就发现问题────────────────────────────────────────二、技术原理调试三板斧2.1 第一斧print调试最简单但也最有效def calc_avg(data):print(f[调试] 输入数据: {data}) # 看输入result sum(data) / len(data)print(f[调试] 计算结果: {result}) # 看输出return result哪步不对一看print就知道。缺点是调试完要删掉这些print不能忘。2.2 第二斧assert断言更高级的自检——条件不满足就直接报错def calc_avg(data):assert len(data) 0, 数据不能为空assert all(isinstance(x, (int, float)) for x in data), 数据里混了非数字return sum(data) / len(data)# 正常调用——没问题calc_avg([1250, 1248, 1251])# 传空列表——assert弹出来calc_avg([]) # AssertionError: 数据不能为空**为什么值得学** assert 像代码里的安检门——数据进来先检查一遍不对立刻停。这样不会出现算了一个错的结果还在继续用的情况。2.3 第三斧breakpoint断点def complex_calculation(data):total 0breakpoint() # 程序跑到这里会暂停进入交互式模式for x in data:total xreturn totalcomplex_calculation([1, 2, 3])# 当你看到 提示符时可以输入变量名看值# data - [1, 2, 3]# total - 0# quit() - 退出继续跑**实用技巧**在 后输入变量名就能看值输入 quit() 或按 CtrlZ 继续运行。────────────────────────────────────────三、实战案例给你的代码写测试3.1 先写一个简单函数def process_lot(data, lot_idUnknown):处理一批Lot数据返回: 均值和是否合格# 自检assertassert len(data) 0, fLot {lot_id} 数据为空assert all(isinstance(x, (int, float)) for x in data), 数据必须为数字avg sum(data) / len(data)std (sum((x - avg)**2 for x in data) / (len(data) - 1)) ** 0.5passed std 3.0 # 标准差3算合格return {lot_id: lot_id, avg: round(avg, 2), std: round(std, 2), pass: passed}3.2 写测试文件新建 test_process_lot.pyimport unittestfrom process_lot import process_lot # 假设函数在上面的文件里class TestProcessLot(unittest.TestCase):测试 process_lot 函数def test_normal_lot(self):正常数据result process_lot([1250.5, 1248.3, 1251.2], FAB-001)self.assertEqual(result[lot_id], FAB-001)self.assertTrue(result[pass]) # 应该合格def test_bad_lot(self):标准差大的数据result process_lot([1250, 1240, 1260], FAB-002)self.assertFalse(result[pass]) # 应该不合格def test_single_value(self):边界情况只有一个值result process_lot([1250], FAB-003)self.assertEqual(result[std], 0) # 标准差为0self.assertTrue(result[pass]) # 标准差0算合格if __name__ __main__:unittest.main()3.3 运行测试python test_process_lot.py输出...----------------------------------------------------------------------Ran 3 tests in 0.001sOK # 全部通过**把test跑失败**在 process_lot 里改点什么比如把 avg sum(data) / len(data) 改成 avg sum(data) / 2再跑测试看看——你会看到 FAILED (failures1)。一个好的函数应该有对应的测试。每次改代码跑一遍测试就知道有没有改坏别的。────────────────────────────────────────四、效果对比| 代码 | 之前 | 之后 ||------|------|------|| 排查Bug | 3天 | 5分钟 || 上线信心 | 但愿没问题 | 测试全过 || 改代码 | 改完忐忑 | 改完跑测试 |────────────────────────────────────────五、自己动手# 练习给你自己的函数写测试# 第1步写一个函数def calc_cpk(data, usl1253, lsl1247):计算Cpkavg sum(data) / len(data)std (sum((x - avg)**2 for x in data) / (len(data) - 1)) ** 0.5return min((usl - avg) / (3 * std), (avg - lsl) / (3 * std))# 第2步写测试# 测试用例1正常数据Cpk应该 1.0# 测试用例2数据波动很大Cpk应该 1.0# 测试用例3传空列表应该怎么办# ✏️ 下面写你的代码**思考题**1. 测试文件 test_xxx.py 为什么以 test 开头试试改成别的名字2. 如果函数里用了 print 而不是 return能测试吗3. 你猜 self.assertEqual 和 assert a b 有什么区别────────────────────────────────────────六、常见误区| 问题 | 表现 | 解决 ||------|------|------|| 没写测试就上线 | 线上出Bug才慌 | 写一行测试不费时间 || 只有正常情况测试 | 边界条件全漏了 | 想想最奇怪的数据也测 || assert不写错误信息 | 只看到AssertionError不知道为啥 | assert x 0, x必须大于0 || 测试和生产用不同的逻辑 | 测试过了上线挂了 | 测试要测真实的函数 |──────────────────────────────────────── **你有查Bug查到崩溃的经历吗评论区讲讲** **收藏点赞下篇学面向对象**