修复后测试全绿就够了吗?用两个故意写坏的版本检查回归质量

0 阅读5分钟

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 辅助整理,三个版本的运行结果已在本地复核,未将示例通过扩展为整个系统的上线结论。