
等价类测试这四个字几乎每一个做软件测试的人都听过面试时也基本都会问但真正能用对、用透的人并不多。我见过不少候选人把等价类划分解释成把输入数据按大小分成几组每组取一个值测一下这个回答只能拿到一半分。等价类测试的核心根本不是分组而是用最少数量的测试用例去覆盖尽可能多的程序处理分支。它的理论依据很朴素对于同一类输入程序的处理逻辑是一致的那何必把这一类里的每个值都测一遍这篇内容写给三类人——刚入行、写测试用例总觉得没底的测试新人准备面试、怕被深挖细节的求职者以及工作中想提升用例设计效率、同时少踩坑的测试工程师。我会从原理讲起用一个完整的实战案例拆解等价类划分的全过程再聊它和边界值分析怎么配合最后分享几个我在实际项目中踩过的坑和对应的应对方式。1. 等价类划分的底层逻辑程序处理路径一致才叫等价1.1 等价类的真正含义等价类测试是黑盒测试里非常经典的方法按教科书的定义它是把程序的输入域划分为若干个子集每个子集称为一个等价类然后从每个等价类中选取少量代表值作为测试用例。但这里最关键的词不是划分而是等价。所谓等价指的是程序对某个集合内的所有输入走的是同一条处理分支。举个例子注册页面的年龄输入框开发可能会写这样一段逻辑if (age 1 || age 150) { // 提示年龄不合法 return; } // 正常注册流程在这段代码里-5、0、151、200这些值虽然数值差异很大但它们都会进入年龄不合法这个分支处理路径完全一致所以它们属于同一个无效等价类。反过来1和150虽然一个在最左边、一个在最右边但都会进入正常注册流程属于同一个有效等价类的两个边界点。很多人划分等价类时容易凭直觉按大小段分比如把1-50看成一组、51-100看成一组、101-150看成一组这种分法本身并没有依据程序的处理逻辑。只要代码里没有对51-100做单独判断那这组细分就没有任何意义反而白白增加了用例数量。所以划分等价类前先问自己一句这些输入在代码里会不会走上不同的分支如果不会它们就是等价的。1.2 为什么能只取一个代表值这是等价类测试最容易被质疑的地方你凭什么说挑一个值测了整个类就没问题了原因在于程序的分支是有限的而输入空间往往是无限的。还是拿1-150的整数输入来说如果老老实实全量测试要跑150个有效输入再加无数个无效输入这在真实项目里根本不现实。但如果按等价类划分合法整数是一类代码无论收到25、87还是143走的都是同一条成功分支所以取一个代表值就能验证这条分支的正确性。同理非法整数、小数、空值、非数字字符串各自都有对应的错误处理分支每个分支测一次就够。这背后的思想其实就是数学里的集合划分每个等价类互不相交所有等价类的并集覆盖整个输入域。用一张图来表示的话就是把一块完整的输入空间切成了若干块每一块内部的点对程序来说长得一样于是只需要从每块里挑一个代表。但这里有一个容易忽略的坑代表值不是随便取的。一个类里的所有值虽然理论上等价但如果你选的那个值恰好附带了另一个隐藏属性就可能把问题引到别的分支上去。比如身份证号是18位字符串其中最后一位可能是X如果把18位字符串当成一个等价类随手选一个纯数字的身份证号做代表那么代码里针对含字母X的特殊处理就完全没被覆盖到。所以选代表值的原则是这个值必须能触发该类独有的处理分支同时不触发其它类的分支。1.3 划分等价类的常规步骤我在实际项目里一般按四步走从需求文档里找出每个输入字段的约束条件比如类型、长度、范围、格式、是否必填。把每个约束拆成满足约束和违反约束两个方向分别对应有效等价类和无效等价类。结合代码实现或接口定义确认这些方向是否真的会走不同分支避免无效细分。为每个等价类挑代表值优先选择能代表该类典型特征的值。这四步做完等价类表格基本就成型了后面写用例只是体力活。2. 有效等价类与无效等价类一个管功能一个管健壮性2.1 有效等价类怎么设计才不浪费用例有效等价类是程序应当接受并正常处理的输入集合它的职责是验证程序在正常数据下能干活。我刚做测试的时候习惯给每个合法值都写一条用例比如年龄输入合法我可能写1、18、35、66、149各一条后来发现这些用例查出来的bug几乎没有区别因为它们全部落在同一条成功分支上。有效等价类的设计原则是尽量合并。多个合法约束同时成立本身互不冲突所以一个合法输入可以同时覆盖多个有效等价类。比如75这个输入既是整数、又在1-150范围内一条用例就把范围内和整数类型两个有效类都覆盖了。这能让用例数量大幅下降把精力留给更值得测的地方。2.2 无效等价类为什么不能合并无效等价类的设计原则恰恰相反一个用例只覆盖一个无效等价类。这是等价类测试里非常重要的纪律。原因很好理解无效输入通常对应不同的报错分支如果一条用例同时输入负数又是空值程序往往只会提示其中一个错误另一个错误分支有没有实现根本看不出来。比如程序先判空提示年龄不能为空那负数校验可能压根没写但这条用例还是会因为至少报了一个错而通过于是bug就被掩盖了。只有一条用例只引入一个无效条件才能准确判断每个错误分支是否都正常工作。这里可以多说一句面试时如果面试官让你现场设计测试用例你脱口而出无效类要单独覆盖因为要避免掩蔽效应这就会是一个很好的加分回答。2.3 新手最常踩的用例设计误区误区一只测有效类不测无效类。很多刚入行的同学潜意识里觉得测正常流程就够了但真实项目里用户可不会按你的剧本输入。无效输入一旦处理不当轻则报错信息不友好重则接口异常、数据脏写甚至系统崩溃。从缺陷密度来看无效路径里的bug数量往往比有效路径更多所以无效等价类在用例设计里的权重至少要和有效类持平。误区二把范围当成唯一的约束。一个输入框除了值的大小还有类型、精度、长度、是否可空、字符串编码方式等约束。比如年龄除了1-150还要考虑必须是整数金额除了上下限还要考虑最多两位小数。每一个约束都对应独立的无效等价类只盯着范围必然会漏测。误区三认为等价类测试只适合输入框。文件上传时的文件类型、下拉菜单里的选项、接口请求里的枚举字段、数据库表里的字段长度都是等价类划分的用武之地。比如上传头像要求jpg或png格式那么一个gif文件就是一个典型的无效等价类而且是最容易漏测的那一类。3. 实战一个年龄输入框如何设计出完整用例3.1 从需求到约束的拆解拿注册页面最常见的年龄输入框举例。需求描述通常是这样的年龄为必填项只允许输入1-150之间的整数。别看这句话简单里面包含了至少四个独立约束必填不能为空类型必须是整数数值范围下限是1数值范围上限是150此外有些产品会明确要求不允许小数、不允许负数、不允许科学计数法格式这些也应该纳入等价类划分。但如果需求文档没提就要主动和产品确认。最怕的是测试按自己的理解去补约束结果产品和开发的实现跟你不一致最后就是扯皮。3.2 完整的等价类划分表基于上面的约束我可以列出一张完整的等价类表。编号类型等价类描述代表值EC-01有效范围内整数下限值1EC-02有效范围内整数中间值75EC-03有效范围内整数上限值150EC-04无效小于下限的整数0EC-05无效大于上限的整数151EC-06无效小数不论是否在范围内18.5EC-07无效负数-5EC-08无效非数字字符串abcEC-09无效空值/未填写空EC-10无效特殊字符#EC-11无效超长数字串123456789012345678EC-12无效科学计数法1e2注意EC-04和EC-07从集合论角度看负数一定小于1如果代码只用age 1做判断-5和0走的是同一个分支其实可以合并。但真实项目中产品的报错提示可能不一样开发也可能额外区分负数和0到1之间这种情况下合并就会丢覆盖。我的经验是划分颗粒度参考需求中的错误提示语如果需求只统一提示年龄不合法那合并没问题如果需求明确说不能为负数和不能小于1要有不同提示那就必须拆开。3.3 用例生成有效类合并无效类单独测有了等价类表生成用例就按两条规则走有效等价类可以合并无效等价类每条只覆盖一个。有效类用例TC-01输入75。预期校验通过年龄正常提交。覆盖EC-02同时覆盖整数和范围内两个约束。TC-02输入1。预期校验通过。覆盖EC-01。TC-03输入150。预期校验通过。覆盖EC-03。无效类用例TC-04输入0。预期提示年龄不能小于1或年龄不合法。TC-05输入151。预期提示年龄不能大于150或年龄不合法。TC-06输入18.5。预期提示请输入整数。TC-07输入-5。预期提示年龄不合法。TC-08输入abc。预期前端阻止输入或提示请输入数字。TC-09不填写。预期提示年龄为必填项。TC-10输入#。预期提示请输入数字。TC-11输入123456789012345678。预期长度校验拦截或提示请输入有效年龄。TC-12输入1e2。预期提示格式不正确如果需求不允许科学计数法。整套用例加起来只有12条覆盖了这个输入框的全部独立输入域。如果不用等价类单是数字范围就能写出无数条效率完全不是一个量级。3.4 执行时需要重点观察哪些结果用例设计只是第一步执行时也不能只看有没有报错。我通常会做三件事收集前端提示和后端返回的HTTP状态码确认是否为预期错误码。去数据库查一下确认无效输入没有被写入业务表。观察报错提示是否友好是否直接把后端异常堆栈抛给用户。这里特别想提醒一点接口层的等价类测试要专门关注应该被拒绝但实际被插入脏数据的情况。有些开发只做了前端校验后端接口没做同样校验导致绕过页面直接调接口就能把年龄写成999甚至-1。这是我在实际项目中见过不止一次的严重缺陷所以我会在用例中明确标注绕过前端直接请求接口这个测试维度等价类表同样适用。4. 边界值法是等价类的最佳搭档两者怎么配合4.1 大量bug都集中在边界两侧等价类测试能保证每一类都覆盖到但对边界值附近的场景并不敏感。比如年龄1-150的合法输入等价类只需要取一个中值代表根本不会刻意去测1和150本身。然而从经验看程序最容易出错的位置恰恰就是边界左右两侧也就是所谓的差一错误英文是off-by-one error。差一错误在代码里太常见了判断条件应该是age 1 age 150结果写成了age 1 age 150于是1和150变成非法2和149反而合法或者反过来把写成导致151这种值也被放行。这类bug用普通的等价类代表值是很难发现的因为中值离边界足够远怎么判断都不会触发边界逻辑。打个比方等价类测试是在每个区域里抽查一个住户而边界值法是把区域之间的围墙仔细检查一遍。社区治安好不好往往在边界地带最能看出问题。4.2 边界值选取规则与标准集合对于一个取值范围[a, b]边界值分析一般选取以下六个值a-1比下限小1比如0a下限本身比如1a1比下限大1比如2b-1比上限小1比如149b上限本身比如150b1比上限大1比如151以年龄输入框为例边界值集合就是0、1、2、149、150、151。其中0和151是无效等价类里刚刚越界的值1、2、149、150是有效等价类里的临界值。如果边界本身是小数比如金额要求0.01-10000.00那么加1要变成加一个最小单位即0.00、0.01、0.02、9999.99、10000.00、10000.01。选取的原则是围绕边界取恰好满足和刚刚不满足两个方向的最小步长值。4.3 一个复合边界案例优惠金额输入框年龄输入框比较简单我们再来看一个更复杂的例子购物结算页的优惠金额输入框需求规定优惠金额为0.01至10000.00元最多两位小数。这里存在两条边界下限0.01、上限10000.00。按边界值法选取0.00低于下限无效0.01下限有效0.02下限0.01有效9999.99上限-0.01有效10000.00上限有效10000.01上限0.01无效此外最多两位小数还意味着10.999这种三位小数属于无效等价类它和超过上限是完全不同的两个错误分支。如果程序对三位小数的处理是四舍五入而不是拦截那业务语义就从优惠9.999元悄悄变成了优惠10元这就是一个隐蔽的功能缺陷。从这个案例能看出等价类划分提供了哪些类别必须测的全景图边界值分析又专门针对边界做了强化两者配合能同时兼顾覆盖率和查错密度。4.4 配合使用的具体操作方式我自己平时操作的习惯是先划分等价类画出完整的等价类表。在有效等价类里同时挑选边界值和非边界代表值比如年龄输入框边界值1和150各一条非边界中值75一条。在无效等价类里优先取刚刚越界的值比如无效下界取0而不是-100无效上界取151而不是1000。刚刚越界的值比极端值更容易暴露判断条件的边界错误。将边界值用例单独打上标签在回归测试版本里重点检查因为开发改代码时最容易动的就是判断条件。这样设计出来的用例数量只比纯等价类多几条但每个关键边界都被反复锤了一遍性价比非常高。5. 等价类测试的三个隐藏陷阱以及我的应对方式5.1 陷阱一等价类无限细分用例数量失控等价类划分理论上可以无限细。还是那个年龄输入框我见过有人把无效类拆成负数-1到-99-100到-999小于-1000每一种都写一条用例最终一张表列了四十多个类用例数量反而失控维护和执行成本都高得离谱。应对方式是做优先级分级。先覆盖核心约束比如类型、范围、是否必填再覆盖次高频的无效输入比如空字符串、超长字符串对那种概率极低、影响也不大的输入比如几十万个字符的超长串可以降级到探索性测试阶段或者用自动化脚本统一生成一批随机数据去跑。等价类测试的目的是用有限成本把主要风险堵住而不是穷举全宇宙的非法输入。5.2 陷阱二只测单输入维度忽略字段组合等价类测试本质上是对单个输入维度做划分但真实业务里一个页面往往有多个输入字段同时生效。登录页面有用户名、密码、验证码查询页面有起止时间、关键字、状态如果每个字段都划分5个等价类3个字段就产生125种组合全测显然不现实。这种情况就不能硬套等价类了需要和其它方法配合使用用配对测试的思想比如PICT工具自动生成两两组合的用例用很少的用例覆盖大部分组合关系。用正交实验法把字段视为因子、等价类视为水平按照正交表设计组合。对关键业务场景比如登录成功、修改密码、提现操作直接用场景法来串流把等价类作为场景里的具体数据取值。等价类负责每一列的覆盖组合测试负责列与列之间的覆盖二者是互补关系不是替代关系。5.3 陷阱三只认输入格式忽视业务规则另一个非常隐蔽的坑是把等价类划分局限在输入格式和范围上忽略了业务规则层面的等价。举一个我印象很深的例子。优惠券输入框表面上只是一个字符串输入格式是优惠券码。如果只按格式划分等价类可能会得出10位字母数字组合有效其它格式无效这样一张表。但实际上优惠券码背后还有业务状态维度未激活的券、已激活还没绑定用户的券、已过期券、已使用券、所属不同活动类型的券这些状态的券在代码里走的完全是不同分支。格式相同的两个券码一个是有效未使用的一个是已经过期的处理结果天差地别。所以现在我在做等价类划分之前一定会先拉一个业务规则清单把输入字段关联的状态流、权限、时间约束、父子记录关系全部列出来再决定按什么维度划分等价类。这比单纯盯着需求文档里的字面约束要管用得多。5.4 一次由数据精度引发的漏测复盘最后分享一个我自己吃过的亏。有一回测试订单金额字段需求只写了金额必须大于0我想当然地划分了正数、负数、0、小数、空值这几个常规等价类用例也全部通过了。结果上线后用户提交了一笔金额为0.001的订单订单创建成功但支付流程直接异常。排查了一圈才发现开发在存储时用了万分数也就是整数乘以10000再入库0.001乘以10000得到10本身没问题但后续对账逻辑里有个四舍五入步骤把10分又还原成了0导致金额变成0后端校验失败。这个案例给我的教训是等价类划分不仅要看用户输入层面的约束还要结合底层数据精度、存储类型、接口协议来补充等价类。用户觉得是大于0的任意金额但代码认为最小单位是0.01这两个约束不一致等价类就会划错。从那以后我凡是遇到金额、数量、百分比这类带精度的字段都会额外确认三个问题存储精度是多少展示精度是多少计算过程中有没有四舍五入或截断这三个问题确认清楚等价类表才能真正贴近代码的行为逻辑。最后分享一个我坚持了很久的习惯每次设计完等价类用例我会把等价类表格单独整理到自己的输入域字典里按日期范围金额手机号身份证号优惠券码这样分类存放。下次遇到类似功能直接拿出来复用、补充设计用例的速度会快很多。另外如果需求文档里的约束写得不清楚划分等价类之前先问自己一句这个输入的合法集合到底是什么拿不准就去找产品确认千万别靠猜。等价类测试看似简单真正的功夫全在需求分析和业务理解上这两点做到位用例设计就成功了一大半。