You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

187 lines
15 KiB
Markdown

2 years ago
# 24 | 紧跟时代步伐微服务模式下API测试要怎么做
你好我是茹炳晟今天我分享的主题是“紧跟时代步伐微服务模式下API测试要怎么做”。
通过一个的Restful API实例我介绍了cURL和Postman工具的基本用法这样我们对API测试有了一个感性认识在此基础上我介绍了API自动化测试框架发展的来龙去脉借此我们对API测试框架的理解又更深入了一层。
今天我将更进一步带你去了解当下最热门的技术领域的API测试即微服务模式下的API测试。微服务架构下API测试的最大挑战来自于庞大的测试用例数量以及微服务之间的相互耦合。所以我今天分享这个主题的目的就是帮你理解这两个问题的本质以及如何基于消费者契约的方法来应对这两个难题。
而为了掌握微服务模式下的API测试你需要先了解微服务架构Microservice Architecture的特点、测试挑战而要了解微服务架构你又需要先了解一些单体架构Monolithic Architecture的知识。所以今天的话题我将逐层展开目的就是希望你可以真正理解并快速掌握微服务模式下的API测试。
## 单体架构Monolithic Architecture
单体架构是早期的架构模式,并且存在了很长时间。单体架构是将所有的业务场景的表示层、业务逻辑层和数据访问层放在同一个工程中,最终经过编译、打包,并部署在服务器上。
比如经典的J2EE工程它就是将表示层的JSP、业务逻辑层的Service、Controller和数据访问层的DAOData Access Objects打包成war文件然后部署在Tomcat、Jetty或者其他Servlet容器中运行。
显然单体架构具有发布简单、方便调试、架构复杂性低等优点,所以长期以来一直被大量使用,并广泛应用于传统企业级软件。
但是,随着互联网产品的普及,应用所承载的流量越来越庞大,单体架构的问题也被逐渐暴露并不断放大,主要的问题有以下几点:
* **灵活性差**:无论是多小的修改,哪怕只修改了一行代码,也要打包发布整个应用。更糟的是,由于所有模块代码都在一起,所以每次编译打包都要花费很长时间。
* **可扩展性差**:在高并发场景下,无法以模块为单位灵活扩展容量,不利于应用的横向扩展。
* **稳定性差**:当单体应用中任何一个模块有问题时,都可能会造成应用整体的不可用,缺乏容错机制。
* **可维护性差**:随着业务复杂性的提升,代码的复杂性也是直线上升,当业务规模比较庞大时,整体项目的可维护性会大打折扣。
正是因为面对互联网应用时,单体架构有这一系列无法逾越的鸿沟,所以催生了微服务架构。
其实微服务架构也不是一蹴而就的也经历了很长时间的演化发展中间还经历了著名的SOA架构。但是这个由单体架构到SOA架构再到微服务架构的演进过程并不是本文的重点所以我就不再详细展开了如果你感兴趣的话可以自行去查阅一些相关资料。
## 微服务架构Microservice Architecture
微服务是一种架构风格。在微服务架构下,一个大型复杂软件系统不再由一个单体组成,而是由一系列相互独立的微服务组成。其中,各个微服务运行在自己的进程中,开发和部署都没有依赖。
不同服务之间通过一些轻量级交互机制进行通信,例如 RPC、HTTP 等,服务可独立扩展伸缩,每个服务定义了明确的边界,只需要关注并很好地完成一件任务就可以了,不同的服务可以根据业务需求实现的便利性而采用不同的编程语言来实现,由独立的团队来维护。
图1就很形象地展示了单体架构和微服务架构之间的差异。
![](https://static001.geekbang.org/resource/image/69/9d/6968c125be2de960b48d8df35315159d.png)
图1 单体架构 VS 微服务架构
微服务架构具有以下特点:
* 每个服务运行在其独立的进程中,开发采用的技术栈也是独立的;
* 服务间采用轻量级通信机制进行沟通通常是基于HTTP协议的RESTful API
* 每个服务都围绕着具体的业务进行构建,并且能够被独立开发、独立部署、独立发布;
* 对运维提出了非常高的要求促进了CI/CD的发展与落地。
## 微服务架构下的测试挑战
由于微服务架构下,一个应用是由很多相互独立的微服务组成,每个微服务都会对外暴露接口,同时这些微服务之间存在级联调用关系,也就是说一个微服务通常还会去调用其他微服务,鉴于以上特点,微服务架构下的测试挑战主要来自于以下两个方面:
1. 过于庞大的测试用例数量;
2. 微服务之间的耦合关系。
接下来,我会针对这两项挑战分别展开,包括它们从何而来,以及如何应对这些挑战,最终完成测试。
**第一,过于庞大的测试用例数量**
在传统的API测试中我们的测试策略通常是
* 根据被测API输入参数的各种组合调用API并验证相关结果的正确性
* 衡量上述测试过程的代码覆盖率;
* 根据代码覆盖率进一步找出遗漏的测试用例;
* 以代码覆盖率达标作为API测试成功完成的标志。
这也是单体架构时代主流的API测试策略。为了让你更好地理解这种测试策略我来举一个实际的例子。
假设我们采用单体架构开发了一个系统这个系统对外提供了3个Restful API接口那么我们的测试策略应该是
* 针对这3个API接口分别基于边界值和等价类方法设计测试用例并执行
* 在测试执行过程中,启用代码覆盖率统计;
* 假设测试完成后代码行覆盖率是80%那么我们就需要找到那些还没有被执行到的20%的代码行。比如图2中代码的第242行就是没有被执行到分析代码逻辑后发现我们需要构造“expected!=actual”才能覆盖这个未能执行的代码行
* 最终我们要保证代码覆盖率达到既定的要求比如行覆盖率达到100%完成API测试。
![](https://static001.geekbang.org/resource/image/ab/e2/ab5e06ccd5d4e3925fe86024b54162e2.png)
图2 基于代码覆盖率指导测试用例设计的示例
**而当我们采用微服务架构时原本的单体应用会被拆分成多个独立模块也就是很多个独立的service原本单体应用的全局功能将会由这些拆分得到的API共同协作完成。**
比如对于上面这个例子没有微服务化之前一共有3个API接口假定现在采用微服务架构该系统被拆分成了10个独立的service如果每个service平均对外暴露3个API接口那么总共需要测试的API接口数量就多达30个。
如果我还按照传统的API测试策略来测试这些API那么测试用例的数量就会非常多过多的测试用例往往就需要耗费大量的测试执行时间和资源。
但是在互联网模式下产品发布的周期往往是以“天”甚至是以“小时”为单位的留给测试的执行时间非常有限所以微服务化后API测试用例数量的显著增长就对测试发起了巨大的挑战。
这时我们迫切需要找到一种既能保证API质量又能减少测试用例数量的测试策略这也就是我接下来要分享的**基于消费者契约的API测试**。
**第二,微服务之间的耦合关系**
微服务化后,服务与服务间的依赖也可能会给测试带来不小的挑战。
如图3所示假定我们的被测对象是Service T但是Service T的内部又调用了Service X和Service Y。此时如果Service X和Service Y由于各种原因处于不可用的状态那么此时就无法对Service T进行完整的测试。
![](https://static001.geekbang.org/resource/image/79/9b/79f5990fd864012cb1c7cd87c8e5079b.png)
图3 API之间的耦合示例
我们迫切需要一种方法可以将Service T的测试与Service X和Service Y解耦。
解耦的方式通常就是实现Mock Service来代替被依赖的真实Service。实现这个Mock Service的关键点就是要能够模拟真实Service的Request和Response。当我介绍完基于消费者契约的API测试后你会发现这个问题也就迎刃而解了。
## 基于消费者契约的API测试
那到底什么是基于消费者契约的API测试呢直接从概念的角度解释会有些难以理解。所以我打算换个方法来帮助你从本质上真正理解什么是基于消费者契约的API测试。接下来就跟着我的思路走吧。
首先我们来看图4假设图4中的Service A、Service B和Service T是微服务拆分后的三个Service其中Service T是被测试对象进一步假定Service T的消费者也就是使用者一共有两个分别是Service A和Service B。
![](https://static001.geekbang.org/resource/image/4b/6f/4b60dcc958636c29ae8edd5e52db216f.png)
图4 Service A、Service B和Service T的关系
按照传统的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 A和Service B对Service T所有可能的调用方式。如果能够找出这样的调用集合并以此作为Service T的测试用例那么只要这些测试用例100%通过Service T的质量也就不在话下了。
从本质上来讲这样的测试用例集合其实就是Service T可以对外提供的服务的契约所以我们把这个测试用例的集合称为“基于消费者契约的API测试”。
那么接下来我们要解决的问题就是如何才能找到Service A和Service B对Service T的所有可能调用了。其实这也很简单在逻辑结构上我们只要在Service T前放置一个代理所有进出Service T的Request和Response都会经过这个代理并被记录成JSON文件也就构成了Service T的契约。
如图5所示就是这个过程的原理了。
![](https://static001.geekbang.org/resource/image/1e/86/1e9c9cb78a02e3675e612da46e2c1b86.png)
图5 收集消费者契约的逻辑原理
在实际项目中我们不可能在每个Service前去放置这样一个代理。但是微服务架构中往往会存在一个叫作API Gateway的组件用于记录所有API之间相互调用关系的日志我们可以通过解析API Gateway的日志分析得到每个Service的契约。
至此我们已经清楚地知道了如何获取Service的契约并由此来构成Service的契约测试用例。接下来就是如何解决微服务之间耦合关系带来的问题了。
## 微服务测试的依赖解耦和Mock Service
在前面的内容中我说过一句话实现Mock Service的关键就是要能够模拟被替代Service的Request和Response。
此时我们已经拿到了契约契约的本质就是Request和Response的组合具体的表现形式往往是JSON文件此时我们就可以用该契约的JSON文件作为Mock Service的依据也就是在收到什么Request的时候应该回复什么Response。
下面的图6就解释了这一关系当用Service X的契约启动Mock Service X后原本真实的Service X将被Mock Service X替代也就解耦了服务之间的依赖图6中的Service Y也是一样的道理。
![](https://static001.geekbang.org/resource/image/e1/01/e18bf590c818e5dc26e85a5e07eb1401.png)
图6 基于Mock Service解决API之间的调用依赖
## 代码实例
自此我已经讲完了基于消费者契约的API测试的原理你是否已经都真正理解并掌握了呢
由于这部分内容的理论知识比较多为了帮你更好地理解这些概念我找了一个基于Spring Cloud Contract的实际代码的示例演示契约文件格式、消费者契约测试以及微服务之间解耦希望可以帮到你。
具体的实例代码,你可以从[https://github.com/SpectoLabs/spring-cloud-contract-blog](https://github.com/SpectoLabs/spring-cloud-contract-blog)下载,详细的代码解读可以参考[https://specto.io/blog/2016/11/16/spring-cloud-contract/](https://specto.io/blog/2016/11/16/spring-cloud-contract/)。
这个实例代码基于Spring Boot实现了两个微服务订阅服务subscription-service和账户服务account-service其中订阅服务会调用账户服务。这个实例基于Spring Cloud Contract所以契约是通过Groovy语言描述的也就是说实例中会通过Groovy语言描述的账户服务契约来模拟真实的账户服务。
这个实例的逻辑关系如图7所示。
![](https://static001.geekbang.org/resource/image/db/fa/db1b2b058505fef62805f9077e1982fa.png)
图7 基于Spring Cloud Contract的契约测试实例
## 总结
单体架构,具有灵活性差、可扩展性差、可维护性差等局限性,所以有了微服务架构。
微服务架构的本身的特点比如微服务数量多各个微服务之间的相互调用决定了不能继续采用传统API测试的策略。
为了既能保证API质量又能减少测试用例数量于是有了基于消费者契约的API测试。基于消费者契约的API测试的核心思想是只测试那些真正被实际使用到的API调用如果没有被使用到的就不去测试。
基于消费者契约的测试方法由于收集到了完整的契约所以基于契约的Mock Service完美地解决了API之间相互依赖耦合的问题。
这已经是API自动化测试系列的最后一篇文章了短短的三篇文章可能让你感觉意犹未尽也可能感觉并没有涵盖到你在实际工程项目中遇到的API测试的所有问题但是一个专栏区区几十篇文章的确无法面面俱到。
我通过这个专栏更想达到的目的是:讲清楚某一技术的来龙去脉及其应用场景,但是很多具体操作级别、代码实现级别的内容,还是需要你在实践中不断积累。
所以如果你还有关于API测试的其他问题非常欢迎你给我留言讨论让我们一起来碰撞出思想火花吧
## 思考题
基于消费者契约的API测试中对于那些新开发的API或者加了新功能的API由于之前都没有实际的消费者所以你无法通过API Gateway方法得到契约。对于这种情况你会采用什么方法来解决呢
欢迎你给我留言。