AI SaaS 工程诊断实战(六)
修好了备注保存,测试也通过了。问题是,这个测试是否只能证明“新备注出现了”,却看不见另一张订单被改了,或者原本已确认的状态被改回草稿?
这篇沿用订单备注的教学场景,给修复版和两个故意写坏的变体运行同一组检查。不引入完整的变异测试平台,先回答一个小问题:当前回归检查,对我们担心的两种错误到底有没有反应?
所有程序和故障均为构造,不是线上事故复盘,也不代表任何工具的实际失败率。
从修改语句找影响面
预期补丁是 UPDATE orders SET note=? WHERE id=?。读这句话时,我会分别问 SET 和 WHERE:允许改哪个字段,允许改哪条记录?
SET 对应的保护是“修改备注时,状态不能倒退”;WHERE 对应的保护是“其他订单不能被修改”。再考虑覆盖、清空与未知 ID,就能得到一组与改动有关的回归,而不必从整个仓库随意选几个测试凑数。
本文规则是允许清空备注,未知订单应报错且不改动任何现有行。这些是教学设定;真实项目要先确认自己的业务规则。
先看一个太弱的检查
弱检查只给 A 保存 new,然后读取 A 的 note 是否等于 new。三个版本都能通过:
- 正确版本:只改 A 的 note。
- 变体一:去掉 WHERE,所有订单的 note 都变成 new。
- 变体二:写 note 的同时,把 A 的 status 改成 draft。
目标字段出现了正确值,并不排斥其他字段或记录出现错误。这就是弱检查全绿的原因,不需要推测测试框架有问题。
给保护条件各放一条可观察结果
新检查使用两条不同状态、不同备注的订单:A 是 confirmed/old,B 是 draft/keep。写 A 后分别读取两条记录,随后测试清空备注与未知 ID。
| 检查 | 为什么需要 |
|---|---|
| A 的 note 变为 new | 原功能仍有效 |
| A 的 status 保持 confirmed | 限制字段影响面 |
| B 保持 draft/keep | 限制记录影响面 |
| A 可以清空备注 | 保护本例允许的边界 |
| 未知 ID 抛出 LookupError | 防止静默成功 |
| 未知 ID 调用前后数据相同 | 防止失败时误改数据 |
最后两项要分开:抛出异常不等于没写过数据。程序先记录调用前的查询快照,再比较调用后结果。这样检查的是可观察状态,而不只是异常是否存在。
两个坏版本确实被抓到了
本地实际运行结果:弱检查对三个版本全部返回 true;完整检查对正确版本六项通过;缺少 WHERE 的版本被“其他订单保持”检查拦下;同时修改状态的版本被“状态保持”检查拦下。
坏版本还可能触发其他检查,但这里不以失败数量评价测试好坏。我们关心的是:预先提出的错误机制,是否有相应的检查能够观测到。
完整演示
保存为 demo.py,使用 Python 3.12 运行 python demo.py。程序只使用标准库。每次检查都重新建立内存库,避免一个变体污染另一个变体的前置数据。
import json
import sqlite3
def save(db, order_id, note):
with db:
cursor = db.execute('UPDATE orders SET note=? WHERE id=?', (note, order_id))
if cursor.rowcount != 1:
raise LookupError(order_id)
def no_where(db, order_id, note):
with db:
db.execute('UPDATE orders SET note=?', (note,))
def changes_status(db, order_id, note):
with db:
db.execute("UPDATE orders SET note=?,status='draft' WHERE id=?", (note, order_id))
def weak_check(writer):
db = sqlite3.connect(':memory:')
try:
db.executescript("CREATE TABLE orders(id TEXT PRIMARY KEY,status TEXT,note TEXT);"
"INSERT INTO orders VALUES('A','confirmed','old');"
"INSERT INTO orders VALUES('B','draft','keep');")
writer(db, 'A', 'new')
return db.execute("SELECT note FROM orders WHERE id='A'").fetchone()[0] == 'new'
finally:
db.close()
def regression(writer):
db = sqlite3.connect(':memory:')
try:
db.executescript("CREATE TABLE orders(id TEXT PRIMARY KEY,status TEXT,note TEXT);"
"INSERT INTO orders VALUES('A','confirmed','old');"
"INSERT INTO orders VALUES('B','draft','keep');")
writer(db, 'A', 'new')
target = db.execute("SELECT status,note FROM orders WHERE id='A'").fetchone()
other = db.execute("SELECT status,note FROM orders WHERE id='B'").fetchone()
checks = {'target_note': target[1] == 'new', 'status_kept': target[0] == 'confirmed',
'other_order_kept': other == ('draft', 'keep')}
writer(db, 'A', '')
checks['clear_allowed'] = db.execute("SELECT note FROM orders WHERE id='A'").fetchone()[0] == ''
before = db.execute('SELECT * FROM orders ORDER BY id').fetchall()
try:
writer(db, 'missing', 'x')
checks['unknown_rejected'] = False
except LookupError:
checks['unknown_rejected'] = True
checks['unknown_did_not_change_rows'] = before == db.execute('SELECT * FROM orders ORDER BY id').fetchall()
return checks
finally:
db.close()
if __name__ == '__main__':
result = {}
for writer in (save, no_where, changes_status):
result[writer.__name__] = {'weak_check': weak_check(writer), 'regression': regression(writer)}
assert all(result['save']['regression'].values())
assert all(item['weak_check'] for item in result.values())
assert not result['no_where']['regression']['other_order_kept']
assert not result['changes_status']['regression']['status_kept']
print(json.dumps(result, indent=2))
这算不算证明测试完整了
还没有。只检查了两个手工构造的错误变体,没有报告整个项目的变异得分,也没有覆盖所有可能的缺陷。权限遗漏、提交失败、并发覆盖、错误 ID 类型、数据库断连等问题,都可能绕开当前检查。
测试本身也可能写错。例如本应保留状态,却在断言里复制了错误实现的默认值;生成器和验证器就会共享同一个误解。构造坏版本的价值,是检验一部分检查敏感性,不是替代需求评审。
真实项目可以留下一个很短的回归说明:
本次改动:备注更新语句
预期影响:目标订单的 note
需要保护:status、其他订单、未知 ID 失败后的现有数据
已执行:对应检查与代码版本
检查敏感性:去掉 WHERE、误改 status 时能失败
尚未执行:权限、并发、重启及部署环境
当 AI 给出“测试全通过”的总结时,我更愿意追问:运行的是哪些检查,使用什么前置数据,哪些错误确实会让它失败?把这几个问题写进记录,比单独贴一个绿色结果更能帮助下一位接手的人。
—— Linksass|AI SaaS 独立开发者。AI 辅助整理,三个版本的运行结果已在本地复核,未将示例通过扩展为整个系统的上线结论。