等价类和边界值,在 AI 一键生成用例的时代还值不值得手画 📅 发布时间:2026/9/14 2:37:47 👁 浏览次数: 关注 霍格沃兹软件测试开发 公众号回复「资料」, 领取人工智能测试开发技术合集把「金额输入框0.01 到 2000 元最多两位小数」这一句需求丢给 AI它半分钟能吐出几十条用例命名规整、分组清楚看上去比你手画一下午的成果体面得多。可你从头翻到尾会发现它测了 100、200、500、800、1000唯独没测 2000.01它写了金额为负的用例却没写小数点后三位的。这不是模型笨。是它做的事和等价类划分、边界值分析做的事压根不是同一件事。这两章写在 Myers 那本《软件测试的艺术》里年头比大多数在职测试工程师都长今天反而更该重讲一遍——不是重讲怎么手画是重讲它在「AI 生成用例」这条流水线上该站哪个位置。一、先把场景钉死一个金额输入框的四条约束不聊抽象方法论就聊一个具体接口。电商运营后台的「优惠券批量发放」POST /api/v1/coupon/grant请求体里一个 amount 字段需求文档给了四条约束amount 必填单位为元只接受数字有效区间 0.01 ≤ amount ≤ 2000.00小数最多两位第三位直接判非法单笔金额严格大于 500.00 元走二级审批流其余直接入账。选金额字段当例子是因为它最容易被 AI 生成得「看着很全」。取值空间连续代表值随手就能编编出来还都落在合理区间里肉眼扫一遍挑不出毛病。真正的问题藏在两端和精度上——恰好是肉眼最容易滑过去的地方。顺手把取值空间划开只有八类类 ID归类指纹分区代表值期望决策EC-V1normal有效100.00acceptedEC-V2approval有效1200.50acceptedapprovalEC-I1below-min无效0.00rejectedEC-I2above-max无效2000.01rejectedEC-I3scale-overflow无效0.001rejectedEC-I4not-a-number无效“abc”rejectedEC-I5empty无效“”rejectedEC-I6missing无效Nonerejected注意 EC-I1 那一行0.00、-1、-999、-0.01 全归 below-min 这一类。它们不是四条用例是同一条用例的四种写法。这句话在手工时代是用来省时间的在 AI 时代是用来判冗余的。二、AI 生成的那一屏用例问题到底出在哪把上面那句需求原样丢给 AI让它「生成完整的测试用例」产出通常有四种典型症状。症状一同一个等价类里堆一堆。 100、200、300、500、800、1000 六条用例归类指纹全是 normal。它们跑同一条代码路径、验同一个判断分支多出来的五条不增加任何信息量只增加执行时间和维护面积。症状二边界整体缺位。 AI 偏爱语言里的高频数字100 和 500 是高频的2000.01 不是。于是下界、审批线、上界这三处最容易出缺陷的位置往往一条都没有。更隐蔽的是精度边界100.00 和 100.01 看着差不多但 100.001 的小数指数跨到了 -3一跨就是另一条分支。症状三类型和格式类被整段跳过。 空串、None、布尔值、科学计数法字符串 1e3、带千分位的 1,000.00、前后带空格的 100 ——这些值一半被前端拦掉、一半被网关拦掉剩下的漏到业务层。而 AI 生成的用例集里这一大片通常只有一条 “abc”。症状四断言空心化。 无效用例只断言状态码不等于 200甚至只断言「不抛异常」。可 400 和 500 是两件事拒绝原因是 below-min 还是 scale-overflow 也是两件事。断言里没有原因码这条用例就测不出回归——将来有人误删了精度校验用例照样全绿。四种症状同一个根因AI 在续写「看起来像测试用例的文本」不在划分取值空间。它手上没有你的约束模型只有语言习惯。你不给它坐标系它就只能按语感撒点。三、等价类的新身份从「省用例的工具」变成「喂给 AI 的约束」手工时代等价类的价值是减法——把无穷的取值压成八个代表值让人少写点用例。AI 时代它多了一层价值而且是更值钱的一层它做加法加在生成过程的约束上。因为等价类表一旦写成结构化数据它就同时是三个东西给 AI 的 prompt 输入。 把这张表贴进去明确要求「每个类 ID 至少一条、同类不超过两条」生成结果的分布立刻从「语感撒点」变成「按格填空」。参数化用例的数据源。 表改了用例自动跟着改不必去几十个测试函数里手动改常量。事后审计的分桶依据。 AI 吐回来的每一条用例都能被归到某个类 ID 上覆盖没覆盖、冗余不冗余一眼可算不用开会吵。一物三用这才是等价类今天还值得画的真正理由。它不是要你回去手抄用例是要你把脑子里那张图变成机器能读的约束。四、把等价类表写成代码再让它长成参数化用例第一步把约束和归类逻辑固化成一份可导入的规格文件。这里附一个最小可跑的校验替身真实项目里把它换成你的接口调用或前端校验函数即可spec/amount_spec.py金额字段的取值约束等价类表 边界值表 归类指纹这张表同时是三样东西给 AI 的约束、参数化用例的数据源、审计脚本的分桶依据from decimal import Decimal, InvalidOperationMIN_AMOUNT Decimal(“0.01”)MAX_AMOUNT Decimal(“2000.00”)APPROVAL_LINE Decimal(“500.00”) # 严格大于才走二级审批等价类表(类ID, 归类指纹, 分区, 代表值, 期望决策)EQUIVALENCE_CLASSES [(“EC-V1”, “normal”, “valid”, Decimal(“100.00”), “accepted”),(“EC-V2”, “approval”, “valid”, Decimal(“1200.50”), “acceptedapproval”),(“EC-I1”, “below-min”, “invalid”, Decimal(“0.00”), “rejected”),(“EC-I2”, “above-max”, “invalid”, Decimal(“2000.01”), “rejected”),(“EC-I3”, “scale-overflow”, “invalid”, Decimal(“0.001”), “rejected”),(“EC-I4”, “not-a-number”, “invalid”, “abc”, “rejected”),(“EC-I5”, “empty”, “invalid”, “”, “rejected”),(“EC-I6”, “missing”, “invalid”, None, “rejected”),]边界值表每个边界取「上一点 / 边界 / 下一点」BOUNDARIES [(“下界”, [Decimal(“0.00”), Decimal(“0.01”), Decimal(“0.02”)]),(“审批线”, [Decimal(“499.99”), Decimal(“500.00”), Decimal(“500.01”)]),(“上界”, [Decimal(“1999.99”), Decimal(“2000.00”), Decimal(“2000.01”)]),(“小数精度”, [Decimal(“100.00”), Decimal(“100.01”), Decimal(“100.001”)]),]class Decision:“”“校验结果 决策 可追溯原因码。原因码是给审计脚本用的不能省。”“”def __init__(self, decision: str, reason: str ): self.decision decision self.reason reasondef fingerprint(raw) - str:“”“归类指纹类型 - 格式 - 精度 - 区间四段决定它落在哪个等价类”“”if raw is None:return “missing”if isinstance(raw, str) and not raw.strip():return “empty”try:value Decimal(str(raw))except InvalidOperation:return “not-a-number”if not value.is_finite(): # 拦住 NaN / Infinityreturn “not-a-number”if value.as_tuple().exponent -2: # 小数位超过 2 位return “scale-overflow”if value MIN_AMOUNT:return “below-min”if value MAX_AMOUNT:return “above-max”if value APPROVAL_LINE:return “approval”return “normal”def validate_amount(raw) - Decision:“”“被测校验逻辑的最小可跑替身真实项目换成你的接口或前端校验”“”fp fingerprint(raw)if fp “normal”:return Decision(“accepted”, fp)if fp “approval”:return Decision(“acceptedapproval”, fp)return Decision(“rejected”, fp)第二步让 pytest 从这张表里长出用例而不是把数据写死在测试函数里tests/test_amount.py从等价类表和边界值表长出参数化用例数据不写死在测试函数里import pytestfrom spec.amount_spec import (BOUNDARIES, EQUIVALENCE_CLASSES, fingerprint, validate_amount,)def ec_cases():“”“等价类 - 参数化数据类 ID 进用例名报告里看得见覆盖了哪一格”“”return [pytest.param(value, expected, idf{ec_id}-{fp})for ec_id, fp, _partition, value, expected in EQUIVALENCE_CLASSES]def boundary_cases():“”“边界值 - 参数化数据三点法一个边界展开成三条”“”return [pytest.param(point, idf{name}-{point})for name, points in BOUNDARIESfor point in points]pytest.mark.parametrize(“amount, expected”, ec_cases())def test_equivalence_classes(amount, expected):“”“每个等价类必须落到它被声明的那个决策上”“”assert validate_amount(amount).decision expectedpytest.mark.parametrize(“amount”, boundary_cases())def test_boundaries_are_decided(amount):“”“边界用例的断言不是「不报错」而是「必须给出明确决策 原因码」”“”result validate_amount(amount)assert result.decision in (“accepted”, “acceptedapproval”, “rejected”)assert result.reason, “边界值必须带原因码否则测不出回归”assert result.reason fingerprint(amount), “原因码必须与归类指纹一致”def test_table_is_self_consistent():“”“自检表里每个代表值必须真落在它声明的那个类上。表错了用例全白搭。”“”for ec_id, fp, _partition, value, _expected in EQUIVALENCE_CLASSES:assert fingerprint(value) fp, f{ec_id} 的代表值 {value!r} 归类不符这么写的好处第一是改动成本。产品把上限从 2000 调到 5000你改一个常量等价类代表值、边界三点、参数化用例、审计基线全部同步。手工时代你要在 Excel 里翻出二十七处「2000」还分不清哪处是需求、哪处是笔误。第二是报告可读性。pytest -v 打出来的用例名直接带类 ID 和边界点名test_boundaries_are_decided[上界-2000.01] 红了你不翻代码就知道是哪一格塌了。五、边界值的新身份验收 AI 输出的那把尺子等价类管生成边界值管验收。分开做是因为它们的失败模式不同等价类没划好你会漏一整片边界值没量过你以为覆盖了其实没有。边界值在 AI 时代的用法是做成一把能自动卡流水线的尺子。AI或者任何自动生成器包括你们内部那个用例工厂交回一份 JSON脚本按三点法逐个比对tools/audit_ai_cases.py用途把 AI 生成的用例JSON拿等价类表和边界值表量一遍判定等价类是否全覆盖、同类是否冗余、边界点是否被精确命中挂进 CI不达标直接非零退出AI 用例集不允许入库运行PYTHONPATH. python tools/audit_ai_cases.py ai_generated.jsonimport jsonimport sysfrom collections import Counterfrom decimal import Decimal, InvalidOperationfrom spec.amount_spec import BOUNDARIES, EQUIVALENCE_CLASSES, fingerprintMAX_PER_CLASS 2 # 同一等价类超过 2 条即判冗余def load_ai_cases(path: str):“”“约定 AI 按 {“cases”: [{“amount”: …}]} 交付——交付格式本身也是契约的一部分”“”with open(path, “r”, encoding“utf-8”) as f:payload json.load(f)return [case.get(“amount”) for case in payload[“cases”]]def to_decimal(value):try:return Decimal(str(value))except InvalidOperation:return Nonedef audit(path: str “ai_generated.json”) - int:values load_ai_cases(path)counter Counter(fingerprint(v) for v in values)# 刻度一等价类覆盖——有没有整类没被测到 missing_ec [ f{ec_id}({fp}) for ec_id, fp, *_rest in EQUIVALENCE_CLASSES if counter.get(fp, 0) 0 ] # 刻度二边界命中——三点法里的每个点是否被精确取到 required {point for _name, points in BOUNDARIES for point in points} given {d for d in (to_decimal(v) for v in values) if d is not None} missing_boundary sorted(required - given, keystr) # 刻度三冗余度——只数「非边界点」。三点法本身就会让同一个类里出现多个边界值 # 把它们算进冗余会误判冗余要盯的是 AI 在区间内部随手堆的那些舒服值。 inner Counter(fingerprint(v) for v in values if to_decimal(v) not in required) redundant {fp: n for fp, n in inner.items() if n MAX_PER_CLASS} print(fAI 用例总数{len(values)}) print(f未覆盖等价类{missing_ec or 无}) print(f冗余等价类{MAX_PER_CLASS} 条{redundant or 无}) print(f漏掉的边界点{[str(p) for p in missing_boundary] or 无}) if missing_ec or missing_boundary: print([BLOCK] 等价类或边界点未覆盖AI 用例集不可入库) return 1 if redundant: print([WARN] 存在冗余建议抽样保留后降级进回归集) print([OK] 等价类全覆盖、边界点全命中) return 0ifname “main”:sys.exit(audit(sys.argv[1] if len(sys.argv) 1 else “ai_generated.json”))尺子的三个刻度对应三种不合格刻度检查什么不合格时长什么样处置动作等价类覆盖八个类 ID 是否每类至少一条整类为 0通常是 missing、scale-overflow打回要求按类补齐再交冗余度同一类 ID 是否超过 2 条normal 类里堆了六条整数金额抽样保留其余降级进回归集边界命中十二个边界点是否被精确取值命中有 1999.99没有 2000.01直接判不可入库第三条最硬。边界点上「差不多」等于「没测」1999.99 和 2000.01 之间隔着一条 还是 的判断隔着一次线上资损。AI 生成的用例集里只要少一个边界点这份集合就不能算已覆盖——不管它总共有一百条还是三百条条数在这个判断面前完全没有发言权。六、三种做法摆在一起看同一份需求三种做法。差别不在用例条数在下面这几个维度上维度纯手工设计纯 AI 生成等价类约束下的 AI 生成覆盖完整性取决于人的经验和当天状态类型/格式类易漏高频值密集边界与异常类型系统性缺失按类填空八类强制到位边界由脚本兜底冗余度低人会自觉合并同类高同一等价类反复堆代表值可控同类超过 2 条即被审计标红可验收性靠评审会和人眼结论不可复现几乎没有抓手「看着挺全」就是结论尺子是代码通过与不通过可复现、可进 CI需求变更成本高用例散落在文档与脚本各处中重新生成一次但老问题会原样重现低改常量即改全套基线同步更新人的时间投向抄写、排版、维护用例文本逐条阅读、逐条怀疑划等价类、定边界、写验收规则图片最后一行才是重点。等价类和边界值并没有把人从这件事里替掉它把人从「写用例」挪到了「定义什么叫做完了」。前者是体力后者是判断力而判断力恰好是 AI 现在交不出来的那部分。顺带算一句成本账约束这一层的前期投入是一张表加一个百来行的审计脚本。它换来的是每一次生成结果都能被自动判定合格与否——从第二个迭代开始你不用再逐条读 AI 的输出这笔投入就回来了。七、写在最后AI 把用例的产出成本压到了接近于零同时把用例的可信成本抬到了从来没这么高。生成一百条不难难的是说清楚这一百条覆盖了什么、没覆盖什么、凭什么算够。等价类是给生成器的坐标系边界值是给验收者的刻度尺。前者决定 AI 往哪儿撒点后者决定你敢不敢签字。两张表都不复杂复杂的是很多团队在有了 AI 之后把它们当成过时的手工活儿扔了——扔掉的不是画图这件事是唯一能让「AI 生成的用例」变成「可交付的测试资产」的那层约束。用例可以被生成覆盖不能。能被生成的那部分交给 AI不能被生成的那把尺子还得你自己刻。你们组的 AI 生成用例是怎么验的有没有一把能进 CI 的尺子留言区聊聊。