一、问题背景:一个空字符串坑了我3天
去年有件让我印象特别深刻的事。
写了一个MES数据导入脚本,跑了半年没问题。突然有一天,某批次的厚度平均值算出来是负数。
查了3天,最后发现:**有一行的厚度值被MES导成了空字符串,被我转成了0。**
更坑的是,上个月也有一批数据少一个值,但因为数据量大,一个0对平均值影响小,没发现。这次恰好样本少,一个0直接拉偏了整个平均值。
**为什么没发现?** 因为写代码的时候只想了"正常情况",没想"数据不完美"的情况。
**教训**:代码能跑 ≠ 代码没问题。Bug藏得越深,排查越痛苦。
**学完这一篇,你能做到:**
1. 用 `print` + `breakpoint` 快速找Bug
2. 用 `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 = 0
breakpoint() # 程序跑到这里会暂停,进入交互式模式
for x in data:
total += x
return total
complex_calculation([1, 2, 3])
# 当你看到 >>> 提示符时,可以输入变量名看值:
# >>> data -> [1, 2, 3]
# >>> total -> 0
# >>> quit() -> 退出继续跑
**实用技巧**:在 `>>>` 后输入变量名就能看值,输入 `quit()` 或按 `Ctrl+Z` 继续运行。
────────────────────────────────────────
三、实战案例:给你的代码写测试
3.1 先写一个简单函数
def process_lot(data, lot_id="Unknown"):
"""
处理一批Lot数据
返回: 均值和是否合格
"""
# 自检(assert)
assert len(data) > 0, f"Lot {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.5
passed = std < 3.0 # 标准差<3算合格
return {"lot_id": lot_id, "avg": round(avg, 2), "std": round(std, 2), "pass": passed}
3.2 写测试文件
新建 `test_process_lot.py`:
import unittest
from 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) # 标准差为0
self.assertTrue(result['pass']) # 标准差=0算合格
if __name__ == '__main__':
unittest.main()
3.3 运行测试
python test_process_lot.py
输出:
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK # 全部通过!
**把test跑失败**:在 `process_lot` 里改点什么(比如把 `avg = sum(data) / len(data)` 改成 `avg = sum(data) / 2`),再跑测试看看——你会看到 `FAILED (failures=1)`。
一个好的函数应该有对应的测试。每次改代码,跑一遍测试就知道有没有改坏别的。
────────────────────────────────────────
四、效果对比
| 代码 | 之前 | 之后 |
|------|------|------|
| 排查Bug | 3天 | 5分钟 |
| 上线信心 | "但愿没问题" | "测试全过" |
| 改代码 | 改完忐忑 | 改完跑测试 |
────────────────────────────────────────
五、自己动手
# 练习:给你自己的函数写测试
# 第1步:写一个函数
def calc_cpk(data, usl=1253, lsl=1247):
"""计算Cpk"""
avg = sum(data) / len(data)
std = (sum((x - avg)**2 for x in data) / (len(data) - 1)) ** 0.5
return 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查到崩溃"的经历吗?评论区讲讲**
> **收藏+点赞,下篇学面向对象**