等价类和边界值,在 AI 一键生成用例的时代还值不值得手画

简介: 本文揭示AI生成测试用例的盲区:看似全面,实则系统性缺失边界值(如2000.01)、精度异常(如0.001)和类型错误(空、null、非数字)。提出以等价类为“坐标系”约束AI生成,以边界值为“刻度尺”自动验收,实现覆盖可度量、冗余可识别、回归可追溯。

把「金额输入框,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-V1
normal
有效
100.00
accepted
EC-V2
approval
有效
1200.50
accepted+approval
EC-I1
below-min
无效
0.00
rejected
EC-I2
above-max
无效
2000.01
rejected
EC-I3
scale-overflow
无效
0.001
rejected
EC-I4
not-a-number
无效
"abc"
rejected
EC-I5
empty
无效
""
rejected
EC-I6
missing
无效
None
rejected
注意 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, InvalidOperation

MIN_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"), "accepted+approval"),
("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 = reason

def 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 / Infinity
return "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("accepted+approval", fp)
return Decision("rejected", fp)
第二步,让 pytest 从这张表里长出用例,而不是把数据写死在测试函数里:

tests/test_amount.py

从等价类表和边界值表长出参数化用例:数据不写死在测试函数里

import pytest
from spec.amount_spec import (
BOUNDARIES, EQUIVALENCE_CLASSES, fingerprint, validate_amount,
)

def ec_cases():
"""等价类 -> 参数化数据:类 ID 进用例名,报告里看得见覆盖了哪一格"""
return [
pytest.param(value, expected, id=f"{ec_id}-{fp}")
for ec_id, fp, _partition, value, expected in EQUIVALENCE_CLASSES
]

def boundary_cases():
"""边界值 -> 参数化数据:三点法,一个边界展开成三条"""
return [
pytest.param(point, id=f"{name}-{point}")
for name, points in BOUNDARIES
for point in points
]

@pytest.mark.parametrize("amount, expected", ec_cases())
def test_equivalence_classes(amount, expected):
"""每个等价类必须落到它被声明的那个决策上"""
assert validate_amount(amount).decision == expected

@pytest.mark.parametrize("amount", boundary_cases())
def test_boundaries_are_decided(amount):
"""边界用例的断言不是「不报错」,而是「必须给出明确决策 + 原因码」"""
result = validate_amount(amount)
assert result.decision in ("accepted", "accepted+approval", "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.json

import json
import sys
from collections import Counter
from decimal import Decimal, InvalidOperation
from spec.amount_spec import BOUNDARIES, EQUIVALENCE_CLASSES, fingerprint

MAX_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 None

def 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, key=str)
# 刻度三:冗余度——只数「非边界点」。三点法本身就会让同一个类里出现多个边界值,
# 把它们算进冗余会误判;冗余要盯的是 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(f"AI 用例总数:{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 0

if name == "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 的尺子?留言区聊聊。

相关文章
|
4天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1681 6
|
9天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1622 1
|
6天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
724 1
|
10天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
17天前
|
人工智能 自然语言处理 安全
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
本文聚焦阿里云2026年推出的三款自研AI办公产品,清晰拆解千问办公、Qoder Teams、Qoder CN的差异化定位与能力边界:千问办公主打职场全场景提效,支持自然语言指令一键完成PPT生成、数据分析等高频办公任务;Qoder Teams面向程序员团队,深度整合AI代码生成、团队协同与企业知识库能力;Qoder CN则专为金融、政务等强合规场景打造,实现数据不出境与VPC私有化部署。文章同步给出分场景选型指南与最新活动定价,帮助不同类型的企业按需组合产品,实现业务岗、研发岗与强合规场景的AI能力全覆盖。
3885 5
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
|
3天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
702 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
|
9天前
|
人工智能 自然语言处理 安全
阿里云AI数智鉴密:AI 生成内容如何拿到一张"防篡改的身份证"
隐形水印 + C2PA签名:让AI生成内容“持证上岗”。
1146 0
|
10天前
|
缓存 数据可视化 开发工具
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
DeepSeek Harness 的更新分两层:本体更新(npx 自动最新、npm update -g、源码 git pull)与插件更新(插件市场点更新、命令行覆盖安装)。本文按「准备 → 更新本体 → 更新插件 → 更新后检查」四步走,覆盖新手常见疑问。
1292 1
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式

热门文章

最新文章