3. 测试用例设计基础
数值算法的测试用例不是“随便找几个数跑一跑”,而是一份可复现的验证契约:它必须说明测什么、用什么输入、期望什么输出、在什么容差下判定通过。理解测试用例的构成要素,是后续所有验证工作的起点。
一、测试用例的构成要素
一条完整的数值算法测试用例通常包含六个要素:用例标识(唯一编号,便于追溯)、被测对象(具体函数、模块或算法分支)、输入数据(参数值、初值、矩阵、区间端点等)、前置条件(如迭代初值、收敛阈值、浮点环境设置)、期望结果(解析解、高精度参考值或满足的性质)、判定准则(绝对误差、相对误差、残差范数或性质是否成立)。其中判定准则最容易被忽视——数值结果几乎不会逐位相等,必须事先约定“多接近才算对”。
例如测试一个求根函数 bisect(f, a, b, tol),输入可取 f(x)=x^3-x-2、区间 [1,2]、容差 1e-10;期望结果可写为“根约 1.5213797068”;判定准则写为“|x*−x_ref| ≤ 1e-8 且 |f(x*)| ≤ 1e-10”。这样即使实现细节不同,只要满足准则即可判通过。
二、黑盒与白盒思想在数值算法中的适用性
黑盒测试只关心输入与输出的映射关系,不查看内部实现。它适合验证算法的外部契约:给定合法输入是否返回满足精度要求的结果、非法输入是否被正确拒绝、边界输入是否稳定。数值算法中,黑盒用例便于在重构、换语言、换编译器后复用。
白盒测试依据内部结构设计用例,追求语句、分支、路径覆盖。数值算法的白盒价值集中在分支与异常路径:如迭代是否收敛的两条出口、除零保护、矩阵奇异判定、区间端点函数值异号检查等。但纯粹的路径覆盖对数值算法帮助有限——浮点运算的组合爆炸使全覆盖不可行,且很多缺陷表现为精度退化而非分支错误。实践中应以黑盒为主、白盒为辅:黑盒保证结果正确性,白盒保证关键分支和防御逻辑被执行到。
三、等价类划分与边界值分析
等价类划分把输入空间切成若干子集,认为同一子集内任一输入暴露缺陷的能力相近,因此每类取少量代表即可。对数值算法,划分维度往往不止一个:按数值范围(如容差 tol 取 1e-3、1e-8、1e-12)、按问题规模(n=1、n=100、n=10000)、按数据性质(良态/病态、对称/非对称、稀疏/稠密)分别划分,再组合成用例。
边界值分析针对等价类的边缘取值。数值算法的边界尤其危险:区间端点处函数值恰为零、容差取机器精度量级、矩阵接近奇异、迭代次数刚好达到上限。以二分法为例,除常规区间 [1,2] 外,应补充:端点即根(f(a)=0)、区间极窄(b−a 接近容差)、f(a) 与 f(b) 同号(应报错)等用例。这些正是缺陷高发处。
四、数值算法测试用例设计的特殊考量
判定必须带容差。浮点运算不满足结合律,同一算法不同实现可能末位不同。用例应写明误差类型与阈值,并区分“结果误差”与“残差”:线性方程组求解中,残差小不代表解准,病态矩阵下二者可能相差数量级,必要时同时断言两者。
参考值来源要可靠。期望结果可来自解析解、高精度库(如 mpmath 计算到 50 位)、已知性质的构造解,或多种独立算法交叉验证。切忌用被测实现自身生成期望值,那会形成循环论证。
覆盖数值退化与异常。应显式设计 NaN/Inf 输入、上溢下溢、除零、不收敛、迭代上限触发等用例,并明确期望行为是“返回错误码”还是“抛出异常”,而非笼统地“不崩溃”。
记录可复现信息。浮点结果依赖编译器优化、FMA 使用、舍入模式与并行归约顺序。用例应固定并记录这些环境条件,否则“时过时不过”的偶发失败难以定位。
把上述要素落到一份可执行的用例表,是本节最直接的交付物。下表以一个“一维求根”被测对象为例,展示等价类、边界值与特殊值如何共同构成用例集(数值为教学示例,非实测结果):
| 编号 | 类别 | 输入 | 期望/判定 |
|---|---|---|---|
| R-01 | 有效等价类 | f=x³−x−2, [1,2], tol=1e-10 | |x*−1.5213797068|≤1e-8 |
| R-02 | 边界:端点即根 | f=x−1, [1,3] | x*=1,误差≤1e-12 |
| R-03 | 边界:区间极窄 | [1.5,1.5+1e-12] | 正常返回或明确报错 |
| R-04 | 无效等价类 | f(a)、f(b) 同号 | 抛出区间无效异常 |
| R-05 | 特殊值 | f 返回 NaN | 检测并报错,不静默循环 |
| R-06 | 白盒分支 | 迭代上限设为 3 | 触发“未收敛”出口 |
这张表的价值在于:每条用例都有明确的判定依据,任何人按表执行都能得到一致结论。测试用例设计的本质,就是把“我觉得应该对”转化为“在约定条件下可判定对错”的可复现证据。