建议02:用接口的代码和测试分层的思想映射测试用例
建议一说要有规则,相当于解决有无的问题,从无规则到有规则意识上的转变是最重要的,至于具体什么规则反而是次要的,各个团队找到最适合自己的即可。我们团队也制定了一套写接口测试用例的规则。
按接口的代码映射测试用例
按接口的代码(分支覆盖或条件覆盖)映射测试,这个观点隐含的意思是接口测试是白盒测试,那为什么接口不是根据对外的表现如契约来映射呢?在实践中我们发现按契约来测试接口可能会漏掉某些测试用例,原因是契约不容易面面俱到。契约的前置条件中对系统中实体的状态要求讲得比较清楚,但往往容易忽略一些技术上的防御,如生物识别支付接口入参是用户的生物特征,接口的最主要职责是用生物特征换取用户的一个付款码(类似手机上的付款码),在某个生物特征已经换取过付款码后,如果接口还允许该生物特征再次换码,则有造成用户重复支付的风险。
Image image(req.image());
// 检查图片是否为重放的图片
if (image.Lock() == false) {
ret = comm::ERR_REPLAY_IMAGE;
return ret;
}
另外,接口对依赖方处理的各种情况也不容易在契约上列『全』,如生物识支付使用的支付平台的接口查询订单详情,查询时会返回超过频率限制,如果接口在业务流程上对限频有特殊的处理,则也需要有单独的测试用例来验证。契约中列举的一般是预期中的情况(业务流程的分支),但包括上述两种情况的『非预期的逻辑错误』有可能在契约中没有体现。当然如果你的契约就是列得比较全,各种情况都在契约中体现了,那证明你把契约维护得非常好,按契约来映射测试用例也没问题。但在没做到把契约维护得这么好之前,按接口代码来映射测试用例是比较稳妥的办法。
另一种不关心接口实现的测试方法是测试矩阵,即把接口的所有输入项均列出来,并且列出每项输入数据的等价类,按这些等价类的排列组合构造测试矩阵。
如最经典的登录功能测试矩阵:用户名3个等价类(有效、无效、空)╳ 密码3个等价类(有效、无效、空)= 9个测试用例。
然而后台接口的输入数据不仅限于接口的入参,参考《软件测试》52讲¹ 中的内容,后台接口的输入项包括:
- 接口的输入数据
- 从DB中读取的数据
- 依赖方返回的数据
- 配置中的数据
- 代码中的全局变量、静态变量
- 时间、随机数
即我们要测试这么多类数据的等价类排列组合情况下接口是否表现正常。这种测试方法需要在有哪些输入数据项和有哪些等价类的维护上耗费大量的精力,即使在有测试用例自动生成等工具的加持下,维护成本依然很高。相比测试矩阵,按接口的代码来映射测试用例会更实际一点。
测试用例分层
按接口的代码映射测试用例是否应该把接口执行的所有代码按一定的覆盖原则全部测试一遍呢?
接口的每个子处理过程、每个子处理过程的递归子处理过程、外部依赖,外部依赖递归的依赖都是接口处理过程的一部分,如果要用测试用例把每个地方的分支或返回情况都测试到,则按接口代码映射测试用例也会像测试矩阵一样测试用例特别多,本质上变成一个裁剪版的测试矩阵。为解决这个矛盾,我们需要引入测试用例分层。我们团队测试分层的方法来源于《软件方法》在第8章²引入的一个假设,系统由3种类构成:
各种类的职责划分如下:
- 控制类:控制用例(系统用例)流,为实体分配职责(为完成系统用例,也会使用边界类)。
- 实体类:系统的核心,封装核心域逻辑和数据。
- 边界类(外系统):每个有接口的外系统映射成一个边界类。
我们可以应用模型驱动设计的方法将控制类的方法映射为后台接口(另一个主题,可以先简单理解为控制类就是编排了封装核心域逻辑的实体类和需要外系统帮助时使用的边界类,不展开)。在实体类和外系统边界类已有各自测试用例的情况下,实体类的测试用例只需要验证实体类和外系统边界类『集成』在一起后是否正确。
以下是各种类测试的建议:
- 外系统边界类测试:外系统的边界类由外系统自己来做。
- 实体类测试:实体类是封装最多领域逻辑的地方,测试用例应该最多,为了执行效率,使用单元测试来测试实体类。
- 控制类:实体类和外系统边界类的集成测试,只测试集成部分。

下面以验证手机号后4位风险识别策略为例说明测试分层的效果:
生物识别支付使用起来很方便,直接识别即可完成付款,但有时系统判定本次识别有风险时,则需要用户输入手机号后4位做进一步的确认。在实现中,是否需要验证后4位的逻辑是封装在『识别结果』类中。

验证后4位有基于识别风险和产品规则等 5个原因,并且每个原因只用一个测试用例就可以覆盖。在测试验证手机号后4位时:
- 为这5个原因各写1个单元测试。
- 为获取生物识别付款码接口写1个验证后4位的测试用例。
实体类已经把各种验证原因做了较完备的测试,控制类只需要测试识别结果类在返回验证后4位时接口的表现是否正确。
测试分层的另外一个显著效果是:在有核心域有复用时,测试用例的数量会大幅下降。还是验证手机号后4位为例,项目后来又拓展了2个其他的场景:生物识别后借充电宝和生物识别会员身份,同样需要验证手机号后4位。
- 如果测试不分层:总测试用例数=场景数×验证手机号测试用例数=3×5=15
- 如果测试分层:总测试用例数=场景数+验证手机号测试用例数=3+5=8
另外一个需要注意的是,控制类不仅编排了实体类,也编排了外系统的边界类,外系统的接口测试已经由外系统自身解决了,但外系统的接口和系统内的实体类搭配在一起是否能正确地工作,这个也需要控制类的测试来覆盖,从这个角度看,接口测试是不能mock掉依赖的接口的,否则边界类的集成就没被验证到,需要更高层次的集成测试来覆盖。
当然你的系统可能不是按控制类、实体类、边界类来组织的,但只要你的系统有层次,就可以做到测试分层,如整洁架构这种洋葱形架构就可以把业务实体用单元测试来覆盖,外层的用例和控制器则只测试对内层的编排,原理雷同。同时我们也发现有的系统做不到测试分层,只能用测试矩阵等蛮力的方式,究其原因是系统本身没有一个好的层次结构,没有将最复杂的核心域逻辑封装到类似实体类的内部。好的测试用例层次结构其实来源于好的代码层次。
至此,还有一个概念值得被提出来,那就是『测试覆盖率』。测试覆盖率是衡量测试用例对被测对象覆盖程序的一个指标。建议二提到的按接口代码映射接口测试用例加上测试用例分层的思想做到代码覆盖率100%不是问题,但真正要保证软件质量,还要追求功能的覆盖率,即软件功能的每个前置条件、执行过程中每个业务规则的每个条件的组合情况都要覆盖到。像测试矩阵一样,想做到功能全覆盖是非常困难的,笔者所在部门也在实践一种功能全覆盖的方法:
首先,规范化需求模型的语法规则,再根据需求模型遍历出所有的操作路径、接着展开每条路径上的业务规则,对于所有路径上的所有业务规则组合自动生成测试用例的剧本。然后,基于测试剧本人工补充前置条件构造、结果断言等部分形成测试用例。剧本的生成全部用算法,避免因为人的原因漏写测试用例,做到了操作路径和业务规则全覆盖,篇幅原因,不详细展开。
对比之下,建议二的做法是不一定能做到功能全覆盖的,但在需求模型也没有完善得建立起来或需求模型还在快速变化不稳定的时期,建议二是值得考虑和实践的一种方法,实践中我们应该不局限于代码覆盖率,而是追求更高的功能覆盖率:
- 实体类单元测试不在本文展开,但遵循测试公共API、测试行为而不是方法等原则可以做到从功能及其各种路径角度编写测试用例。
- 控制类的测试分支覆盖接近于功能覆盖,如果差距很大,需要检查是否将过多的职责分配给了控制类,考虑将领域逻辑更多地封装到实体类,用实体类的单元测试来覆盖。