微服务架构下,API 测试的最大挑战来自于庞大的测试用例数量,以及微服务之间的相互耦合。

题外话

为了掌握微服务模式下的 API 测试,需要先了解微服务架构(Microservice Architecture)的特点、测试挑战;而要了解微服务架构,又需要先了解一些单体架构(Monolithic Architecture)的知识。

单体架构(Monolithic Architecture)

单体架构是将所有的业务场景的表示层、业务逻辑层和数据访问层放在同一个工程中,最终经过编译、打包,并部署在服务器上。

比如,经典的 J2EE 工程,它就是将表示层的 JSP、业务逻辑层的 Service、Controller 和数据访问层的 DAO(Data Access Objects),打包成 war 文件,然后部署在 Tomcat、Jetty 或者其他 Servlet 容器中运行。

优点:发布简单、方便调试、架构复杂性低

缺点:

1、灵活性差

无论是多小的修改,哪怕只修改了一行代码,也要打包发布整个应用。更糟糕的是,由于所有模块代码都在一起,所以每次编译打包都要花费很长时间。

2、可扩展性差

在高并发场景下,无法以模块为单位灵活扩展容量,不利于应用的横向扩展。

3、稳定性差

当单体应用中任何一个模块有问题时,都可能会造成应用整体的不可用,缺乏容错机制。

4、可维护性差

随着业务复杂性的提升,代码的复杂性也是直线上升,当业务规模比较庞大时,整体项目的可维护性会大打折扣。

微服务架构(Microservice Architecture)

微服务是一种架构风格。在微服务架构下,一个大型复杂软件系统不再由一个单体组成,而是由一系列相互独立的微服务组成。

其中,各个微服务运行在自己的进程中,开发和部署都没有依赖。

不同服务之间通过一些轻量级交互机制进行通信,例如 RPC、HTTP 等,服务可独立扩展伸缩,每个服务定义了明确的边界,只需要关注并很好地完成一件任务就可以了,不同的服务可以根据业务需求实现的便利性而采用不同的编程语言来实现,由独立的团队来维护。


单体架构 VS 微服务架构.png

特点:

1、每个服务运行在其独立的进程中,开发采用的技术栈也是独立的;

2、服务间采用轻量级通信机制进行沟通,通常是基于 HTTP 协议的 RESTful API;

3、每个服务都围绕着具体的业务进行构建,并且能够被独立开发、独立部署、独立发布;

4、对运维提出了非常高的要求,促进了 CI/CD 的发展与落地。

微服务架构下的测试挑战

由于微服务架构下,一个应用是由很多相互独立的微服务组成,每个微服务都会对外暴露接口,同时这些微服务之间存在级联调用关系,也就是说一个微服务通常还会去调用其他微服务。

鉴于以上特点,微服务架构下的测试挑战主要来自于以下两个方面:

1、过于庞大的测试用例数量

在传统的 API 测试中,我们的测试策略通常是:

根据被测 API 输入参数的各种组合调用 API,并验证相关结果的正确性;

衡量上述测试过程的代码覆盖率;

根据代码覆盖率进一步找出遗漏的测试用例;

以代码覆盖率达标作为 API 测试成功完成的标志。

假设我们采用单体架构开发了一个系统,这个系统对外提供了 3 个 Restful API 接口,那么我们的测试策略应该是:

针对这 3 个 API 接口,分别基于边界值和等价类方法设计测试用例并执行;

在测试执行过程中,启用代码覆盖率统计;

假设测试完成后代码行覆盖率是 80%,那么我们就需要找到那些还没有被执行到的 20% 的代码行。

比如图 中代码的第 242 行就是没有被执行到,分析代码逻辑后发现,我们需要构造“expected!=actual”才能覆盖这个未能执行的代码行;


基于代码覆盖率指导测试用例设计的示例.png

最终我们要保证代码覆盖率达到既定的要求,比如行覆盖率达到 100%,完成 API 测试。

而当我们采用微服务架构时,原本的单体应用会被拆分成多个独立模块,也就是很多个独立的 service,原本单体应用的全局功能将会由这些拆分得到的 API 共同协作完成。

比如,对于上面这个例子,没有微服务化之前,一共有 3 个 API 接口,假定现在采用微服务架构,该系统被拆分成了 10 个独立的 service,如果每个 service 平均对外暴露 3 个 API 接口,那么总共需要测试的 API 接口数量就多达 30 个。

如果我还按照传统的 API 测试策略来测试这些 API,那么测试用例的数量就会非常多,过多的测试用例往往就需要耗费大量的测试执行时间和资源。

但是,在互联网模式下,产品发布的周期往往是以“天”甚至是以“小时”为单位的,留给测试的执行时间非常有限,所以微服务化后 API 测试用例数量的显著增长就对测试发起了巨大的挑战。这时,我们迫切需要找到一种既能保证 API 质量,又能减少测试用例数量的测试策略。

2、微服务之间的耦合关系

微服务化后,服务与服务间的依赖也可能会给测试带来不小的挑战。

如图所示,假定我们的被测对象是 Service T,但是 Service T 的内部又调用了 Service X 和 Service Y。此时,如果 Service X 和 Service Y 由于各种原因处于不可用的状态,那么此时就无法对 Service T 进行完整的测试。


API 之间的耦合示例.png

方法:将 Service T 的测试与 Service X 和 Service Y 解耦

解耦的方式通常就是实现 Mock Service 来代替被依赖的真实 Service。实现这个 Mock Service 的关键点就是要能够模拟真实 Service 的 Request 和 Response。

基于消费者契约的 API 测试

核心思想是:只测试那些真正被实际使用到的 API 调用,如果没有被使用到的,就不去测试。

假设图中的 Service A、Service B 和 Service T 是微服务拆分后的三个 Service,其中 Service T 是被测试对象,进一步假定 Service T 的消费者(也就是使用者)一共有两个,分别是 Service A 和 Service B。


Service A、Service B 和 Service T 的关系.png

按照传统的 API 测试策略,当我们需要测试 Service T 时,需要找到所有可能的参数组合依次对 Service T 进行调用,同时结合 Service T 的代码覆盖率进一步补充遗漏的测试用例。

但是这样的话测试用例的数量会非常多,那我们就需要思考,如何既能保证 Service T 的质量,又不需要覆盖全部可能的测试用例。

仔细想一下,会发现 Service T 的使用者是确定的,只有 Service A 和 Service B,如果可以把 Service A 和 Service B 对 Service T 所有可能的调用方式都测试到,那么就一定可以保证 Service T 的质量。即使存在某些 Service T 的其他调用方式有出错的可能性,那也不会影响整个系统的功能,因为这个系统中并没有其他 Service 会以这种可能出错的方式来调用 Service T。

从本质上来讲,这样的测试用例集合其实就是,Service T 可以对外提供的服务的契约,所以我们把这个测试用例的集合称为“基于消费者契约的 API 测试”。

那么接下来,我们要解决的问题就是:如何才能找到 Service A 和 Service B 对 Service T 的所有可能调用了。其实这也很简单,在逻辑结构上,我们只要在 Service T 前放置一个代理,所有进出 Service T 的 Request 和 Response 都会经过这个代理,并被记录成 JSON 文件,也就构成了 Service T 的契约。


收集消费者契约的逻辑原理.png

在实际项目中,我们不可能在每个 Service 前去放置这样一个代理。但是,微服务架构中往往会存在一个叫作 API Gateway 的组件,用于记录所有 API 之间相互调用关系的日志,我们可以通过解析 API Gateway 的日志分析得到每个 Service 的契约。

补充知识:API Gateway 一般是作为整个微服务的入口,做一些权限校验,路由功能,并不是每个服务间都存在的,只有面向客户端的服务才会有这一层。服务间的内部调用不走API Gateway 。

微服务测试的依赖解耦和 Mock Service

实现 Mock Service 的关键,就是要能够模拟被替代 Service 的 Request 和 Response。

此时我们已经拿到了契约,契约的本质就是 Request 和 Response 的组合,具体的表现形式往往是 JSON 文件,此时我们就可以用该契约的 JSON 文件作为 Mock Service 的依据,也就是在收到什么 Request 的时候应该回复什么 Response。

下图 解释了这一关系,当用 Service X 的契约启动 Mock Service X 后,原本真实的 Service X 将被 Mock Service X 替代,也就解耦了服务之间的依赖


基于 Mock Service 解决 API 之间的调用依赖.png