契约测试与独立交付
前面讲解了微服务的定义,其中多次提到一个重要的特点:微服务能够自动地独立部署。随着我们对系统要求的不断提高,自动部署早已不是难题,我们还希望程序能够自动交付到客户手中,即自动部署到生产环境中,这也是现在大家经常提到的CI(ContinuousIntegration ,持续 集成 )和 CD(Continuous Delivery, 持续交付)。
独立交付
随着Jenkins等CI/CD开源工具和框架的流行,CI/CD在技术上已经不是难题,大多数公司或团队都能快速搭建起自己的部署和交付流水线,以达到程序自动部署和交付的目的。但我们忽视了微服务的根本特点,那就是独立。
一个可以自动部署和交付的流水线只能保证程序能够自动化地拉取、构建和运行,但并不能保证应用程序能够正常使用,独立交付失败示例如图4.7所示。
假设现在有一个App,其中有A和B两个服务,A和B之间有依赖关系,正确的依赖关系是服务A的1.0版本依赖服务B的1.0版本。如果服务A想要发布1.1版本,代码经过服务A的流水线,通过单元测试,并且成功部署上线,这时App出问题了,因为此时环境中服务B还是1.0版本,并不兼容1.1版本的服务A。
因此,此时并不能做到服务A的独立部署,还需要服务B做相应的兼容升级,不然会导致应用程序异常。传统的微服务项目都会存在这个问题,并且在交付时都要依靠相当成本的人力投入进行大量的回归测试,来保证服务间依赖的兼容性。
如果我们的服务能真正做到只关心自己的流水线配置、自己的测试结果、自己的自动化部署,而不需要依赖其他服务的升级和发布,也不会影响其他服务的正常使用,才是正常的独立交付。与自动部署相比,微服务要独立显然要难很多。
集成测试
我们能通过什么方式保证服务的正常交付呢?通常大多数团队都会引入集成测试,通过在部署前进行E2E的集成测试,集成测试往往能够更加接近真实的使用环境,所有的依赖都是真实的部署,没有任何Mock做到真实的集成,此时最能发现依赖的问题。
什么是E2E测试?E2E即End Point to End Point,即端到端测试,是一种从头到尾测试应用程序是否按照预期设计执行的方法,其目的就在于确保各种系统组件和系统之间信息的正确传递,整个应用程序在真实场景中进行测试,如与数据库、网络、硬件和其他应用程序进行通信等,都需要真实集成到环境中进行部署和测试。
如果将图4.7加入集成测试,E2E集成测试流程如图4.8所示。
在集成测试中,通常会将部署环节统一管理,在部署之前加入集成测试,这样看起来能解决我们的问题,当服务A发布1.1版本后,E2E环境中的服务版本情况为服务A是1.1版本,服务B是1.0版本,这时测试显然无法通过,集成测试发现了服务之间依赖的问题后,部署流程就会被中断,从而保护应用程序的正常运行。
但这样就算是有独立交付的能力了吗?当然不是,我们将例子改造一下,再加入一些服务,让它看起来更像一个真实的交付场景,多服务集成测试场景如图4.9所示。
在图4.9中,假设现在App包含了4个服务:A、B、C、D,此时A和B还有依赖关系,服务B的1.0版本依赖服务A的1.0版本,此时服务A发布了一个1.1的新版本到集成测试环境,像之前一样,集成测试失败,服务A无法发布到最终的App环境上,此时服务C和服务D也有新功能需要发布,而且服务C和服务D与其他服务之间没有任何依赖,应该可以成功发布到App端。但此时服务A和服务B的错误导致集成的环境是失败状态,因此,当服务C和服务D发布1.1版本时,集成测试仍然失败,服务C和服务D的部署也会被中断,直到服务A和服务B解决依赖问题,服务C和服务D才可以正常部署。这样的问题很明显,虽然集成测试能够检测出服务间的依赖问题,保护生产环境不会出现组件或服务间的依赖出错,但服务仍然不具备独立交付的能力,一旦交付失败,很有可能阻塞整个系统的流水线,使其无法正常工作。
那么,到底该如何让服务拥有独立交付的能力?其实方法很简单,既然在统一部署前集成测试会阻塞整个流水线,那么就把集成测试放到每条流水线,流水线独立集成测试流程如图4.10所示。
每条流水线都拥有自己的E2E测试,而且每个E2E测试的集成环境完全一致,都是模拟的当前生产环境,即App现有的版本环境,当服务A发布不兼容的版本后,服务A的交付流水线会被中断,而不会影响其他服务的正常部署。
这样做看似解决了服务部署之间的阻塞问题,使服务达到了独立交付的目的,但实施难度太大。
为什么我们的项目中集成测试环境一般就1~2个?
首先,集成测试的重点在于测试组件之间信息的正确传递,其更加关注整体的运行情况、配置项等,而非关心每个服务的每个接口的依赖关系,如果这样集成测试的工作量巨大,而且这样的工作每个服务的流水线都要做一遍,显然不现实。其次,搭建一个E2E的集成测试环境的成本很高。E2E是一种从头到尾测试应用程序是否按照预期设计执行的方法,系统中所有的组件都需要真实的部署,包括数据库、存储设备、其他服务接口等,这就意味着要如图4.10那样部署一套App的流水线,其所需要的服务器或虚拟机的数量和资源巨大,而且这些集成环境必须与最终的交付环境保持一致,这无疑又增加了更多维护成本。
总之,集成测试确实是一个耗时耗财耗力的事情,除了这些,集成测试外部因素过多,导致我们在发现问题时难以快速定位问题,很难确认是哪个模块出了问题,而且很可能这些依赖都是不同团队开发的服务或组件,在集成测试出现问题时,问题的沟通反馈与修复的周期也会十分漫长。
当然,集成测试还有很多优势,如在测试环境中所有的组件都是真实的部署,测试结果更接近真实使用情况,而且测试的逻辑简单直接,更容易让人理解。
既然使用集成测试来保证微服务的独立交付在实施时会出现很大的问题,那么有没有更好的方式来替代呢?
真正的独立交付
ThoughtWorks的首席咨询师王健曾发表过一篇文章《你的微服务敢独立交付吗?》,在文章中就提到微服务独立交付的解决方案,也就是本章的“主角”:使用契约测试来使服务能够独立交付。
4.3.2节分析了使用E2E测试的弊端,这些问题在单元测试中往往不存在,所以在多数情况下,开发者都乐于使用单元测试来解决问题。如果拥有一个可以检测出服务之间依赖问题的单元测试,就意味着这个单元测试同样可以达到图4.10中E2E测试的目的,使服务能够独立交付,契约测试正是这样的一种单元测试。
契约测试是服务提供和调用双方共同定义的所依赖接口的描述。
正如4.1节中的介绍,服务提供者可以使用契约检验自己的接口是否符合当初的定义,服务调用者可以使用契约来Mock生成一个MockServer,从而不用等待提供者的接口开发完成就可以调试自己的程序。在流水线的测试中同样可以使用Mock Server做到不用真实地启动依赖服务就达到与真实集成近似的效果,契约测试在交付流程中的应用如图4.11所示。
看起来契约测试可以胜任之前集成测试的工作,所以可以将之前的流水线再次改造,使用契约测试替代E2E集成测试,契约测试在流水线中的流程如图4.12所示。
这样每条流水线都会有自己的契约测试,当测试服务A发布到App端时契约测试会验证服务A提供的接口是否符合接口的定义,当测试服务B发布到App端时契约测试会验证服务B在使用依赖的接口时是否也符合接口的定义。如果服务A发布的1.1版本接口发生了变化,那么契约测试就会失败,从而阻止服务A的部署流水线,并且不会影响其他服务流水线的正常工作,从而达到了真正的独立交付。
契约测试本身是轻量的,不需要依赖外部的组件,相比集成测试实施起来更加容易。那么,我们如何去实现一个契约测试?又有哪些技术框架呢?下面介绍契约测试的相关技术与用法。