代码评审、用例设计、算法分析里到处是「XX case」:edge case、corner case、boundary case、happy path、worst case、base case……而且经常被当成同义词换着用。它们其实分布在几条互不相干的轴上,老家也分别在硬件工程、数学、算法分析和需求分析里,没有一个是测试专有的词。把它们各自摆回所属的轴,混淆就散了。

这些「case」大致落在四条轴上:输入空间里的位置、程序执行的场景走向、输入的刁钻程度、递归的收敛结构。

轴一 · 输入空间的位置

把每一个输入参数看成一根坐标轴:数组长度是一根轴,取值范围是一根轴,并发数是一根轴,时间是一根轴……所有参数张成一个高维空间。合法输入落在这个空间内部,被称作正常情况(normal case)。问题往往不出在中间,而出在这个空间的表面、棱和顶点上。

拿一个二维矩形打比方最直观。矩形的内部是正常区域,四条边是它的边界,四个角是两条边的交点。edge 对应边,corner 对应角,这不是巧合。两个术语最早就是从模拟电路和硬件工程的这套几何语言里借来的。

参数空间里的 normal、edge、corner

normal case(正常情况)

落在矩形内部,各个参数都取典型值、彼此组合也常见的情况。绝大多数请求都是 normal case,它们不是本文的主角,但它是理解其余几个词的参照系:所谓 edge、corner,都是相对「内部」而言的偏离。

edge case(边缘情况)

边缘情况指的是问题只在某一个操作参数达到极端(最大或最小)时才出现,其他参数保持正常。用矩形的话说,就是落在某一条边上:一个维度顶到头,其余维度都在中间。维基百科的定义是:

An edge case is a problem or situation that occurs only at an extreme (maximum or minimum) operating parameter.

典型的 edge case:

  • 空数组、空字符串、空集合,长度这根轴取到最小值 0
  • 满容量写入,长度这根轴顶到上限
  • Integer.MAX_VALUE 参与运算触发溢出,数值轴顶到类型上限
  • 账户余额为零、库存为零,业务量这根轴归零

它们的共同点是:只需要盯住一根轴,把它拨到最小或最大。因为一次只动一个维度,edge case 可以逐维枚举。有 N 个参数,就把每个参数分别拨到 min 和 max,用例数量大致是 N 的线性倍数,是能穷举完的。

corner case(极端情况)

极端情况指的是问题只在多个参数同时处于极端水平时才暴露,尽管每一个参数单独看都还在它各自的合法范围之内。落在矩形上,就是那个角,两条边在这里相交,两个维度同时顶到头。维基百科的定义强调了「同时」这个关键词:

A corner case is a problem or situation that occurs only outside of normal operating parameters—specifically one that manifests itself when multiple environmental variables or conditions are simultaneously at extreme levels, even though each parameter is within the specified range for that parameter.

这里的「outside of normal operating parameters」不是指越过了合法范围,而是指越过了「典型的参数组合」。每个参数都还合法,只是它们凑到一起的这个组合罕见到平时没人碰。维基百科举的那个音响例子很能说明问题:一只扬声器在最大音量下不失真,在低温环境里也正常,但当最大音量、低温、电压波动这几个条件叠在一起时,失真就出现了。任何单一条件都不足以触发它。

软件里的 corner case:

  • 分页查询时,请求最后一页、每页数量恰好整除总数、翻页过程中又有数据被删除,三件事凑在一起导致空页或错位
  • 高并发写入、单批数据量达到上限、下游正在重试,三者叠加时出现重复提交或死锁
  • 定时任务撞上闰秒、恰逢时区切换、又是月末最后一天,日期计算给出荒谬结果

corner case 的麻烦在于组合爆炸。N 维空间的顶点数量是 2 的 N 次方,随维度指数增长,不可能像 edge case 那样逐个枚举。找它们靠的不是穷举,而是推理哪些维度之间存在耦合,也就是哪几个条件同时成立时会互相放大。

boundary case(边界情况)

边界情况关心的是输入正好踩在行为发生突变的那条线上,以及线两侧那一格。按教科书定义,它指输入正好处在、或刚刚越过最大最小限制的位置,尤其是 off-by-one 最爱藏身的 min-1max+1

它和 edge case 常被当成一回事,但有一个关键差别:edge 强调「极端」,boundary 强调「临界点」,而临界点不一定在范围的尽头。测试里那套边界值分析(Boundary Value Analysis)用的正是后一种含义——边界是任意两个行为区间之间的分界线,这条线可以落在合法范围的中间。

boundary 与 edge 的区别

举个能把两者掰开的例子。订单金额字段合法范围是 0 到无上限,「满 100 减 20」这条规则在 100 处切换行为。那么 99 / 100 / 101 是 boundary case,行为在这里翻转;但 100 既不是最大值也不是最小值,它在范围中间,所以不是 edge case。反过来,金额为 0 的空订单既是 edge case(极端),又是 boundary case(从「无订单」到「有订单」的转变点)。

所以 edge 是 boundary 的子集:edge 是坐落在范围两端的那一类特殊边界,boundary 还额外包含范围内部的行为分界线,以及越界那一格。

degenerate case(退化情况)

退化情况来自数学,尤其是几何。它指某个结构塌缩、丢掉了本该有的性质:三角形三点共线退化成一条线段,圆半径为零退化成一个点,矩阵行列式为零。

退化情况:结构塌缩

代码里的退化无处不在:空集合、单元素数组、零长度字符串、自环的图、全部重复的键、只有一个节点的树。它们经常和 edge case 重叠(都在极端),但视角不同——edge 关心「值到没到尽头」,degenerate 关心「结构还成不成立」。很多算法在退化输入下会走进平时测不到的分支:除数为零、循环一次都不进、递归立即返回。

位置轴上的一句话

edge 是一根轴顶到头,corner 是几根轴同时顶到头,boundary 是任何行为翻转的临界线(含范围内部),degenerate 是结构塌缩到失去性质。四者都在描述「输入落在空间的什么位置」,只是关注的位置特征不同。

轴二 · 场景怎么走:happy path 与 sad path

这条轴根本不在测试里出生,而是需求分析和用例建模的词汇,Alistair Cockburn 那套 use case 方法论把它们带火。它描述的不是「输入在哪」,而是「一次执行沿着哪条分支走」。

  • happy path(正常路径,也叫 golden path):一切顺利、无异常、走到成功结局的主干流程。
  • sad path(异常路径,也叫 unhappy path / alternate path / exception path):校验失败、外部出错、需要补偿或回滚的分支。

happy path 和 edge case 是两个维度的东西:happy path 可以走到极端输入(空购物车结算也可能一路顺利),sad path 也可能由再普通不过的输入触发(余额不足是最常见的输入之一)。把二者混为一谈,测试就容易只测「正常输入的正常流程」,漏掉「正常输入的异常流程」。

轴三 · 输入有多刁钻:best / average / worst / pathological

这条轴是算法分析和复杂度理论的核心词,Knuth 那一脉,和测试没有直接关系。它衡量的是同一个算法在不同输入下的表现。

  • best case / average case / worst case(最好 / 平均 / 最坏情况):算法在最有利、平均、最不利输入下的渐进复杂度。快排平均 O(n log n),最坏 O(n²)。
  • pathological case(病理情况):出身也是数学(病态函数,比如处处连续却处处不可导的 Weierstrass 函数),在算法里指专门触发最坏复杂度的刁钻输入。给快排喂一个已经有序的数组、给哈希表构造大量冲突键,都是病理输入。
  • adversarial input(对抗性输入):病理输入的现代变体,被攻击者当武器用。哈希碰撞拒绝服务(hash flooding)、正则灾难回溯(ReDoS)都是拿病理输入把系统性能打垮的安全问题。

pathological case 和 corner case 常被混用,但重点不同:corner case 强调「多个参数同时到极端」,pathological case 强调「这个输入专门用来触发最坏行为」,哪怕它每个参数都很普通。一个已排序数组,每个元素都平平无奇,却是快排的病理输入。

轴四 · 递归怎么收敛:base case

base case(基本情况) 出身是数学归纳法和递归。它是归纳的起点、递归的终点——不再继续拆分、直接返回的那一层。

它跟前面几条轴压根不是一个维度的东西。前面讲的是「输入落在空间的什么位置」或「执行走哪条分支」,base case 讲的是「递归怎么停下来」。放在一起容易混,纯粹是因为写递归时漏掉 base case 和漏掉 edge case 都会炸,但成因完全不同:前者是递归不收敛(栈溢出),后者是极端输入未处理。

家谱:这些词的老家在哪

把四条轴和它们的起源领域摆到一起,就是这些「case」的家谱:

按术语的起源领域列一遍,「是不是测试专有词」这个问题的答案就一目了然:

术语 老家(起源领域) 是否测试原生
edge case / corner case 硬件与模拟电路工程 否,借用
boundary case 数学「边界」概念;边界值分析是测试方法 边界值分析算测试原生
degenerate case 数学(几何、代数)
happy path / sad path 需求工程与用例建模(UX)
best / average / worst case 算法复杂度分析
pathological case 数学,后入算法分析
adversarial input 安全与算法复杂度攻击
base case 数学归纳法与递归

这些词的老家分别是硬件工程、数学、算法复杂度分析、需求与 UX。测试是它们的集散地而不是产地。真正在测试里土生土长的,只有边界值分析这类具体方法,而它依赖的「边界」本身还是数学概念。

对测试和设计的实际影响

分清这些词不是咬文嚼字,因为它们指向不同的投入方式。

edge case 是防守的基本功,成本低、可穷举,应该在单元测试里逐维覆盖:空、满、零、负、溢出,每一根轴都拨到头测一遍。boundary case 更进一步,提醒对每一条行为分界线都测「线前、线上、线后」三格(x-1 / x / x+1),不管这条线在范围尽头还是中间——绝大多数 off-by-one bug 藏在这里。degenerate case 提醒专门构造结构塌缩的输入,看算法会不会走进未处理的分支。

corner case 的成本高、数量爆炸,穷举既不现实也不划算。有效的做法是分析参数之间的耦合,只针对「同时成立会互相放大」的那几组组合设计用例,再辅以模糊测试、随机组合和线上流量回放去撞想不到的角落。pathological case 和 adversarial input 则要在性能与安全测试里单独立项,用最坏输入而不是平均输入去压。happy path 与 sad path 提醒每条业务流程都要覆盖成功与失败两个走向,base case 提醒每个递归都要先确认能停下来。

一个实用的检查顺序:先把每根轴的 edge 逐个测干净,再对每条行为分界线补上 boundary 三格,然后问「哪两三根轴同时顶到头时会出事」重点验证那几个角,最后针对性能与安全场景构造病理输入。

参考资料