第05节:组件测试详解
本课程中所说的组件(Component),是指一个大型系统中,某一个可以独立工作的、封装完整的组成部分。在微服务架构中,组件实际上就代表着微服务本身,或者说单个微服务。以下将其称为“单服务测试”(Single-service Test)。这个测试的实质,就是将一个微服务与其所依赖的所有其他服务或资源全部隔离开,从该服务外部“用户”的角度来审视服务能否提供预期的输出。这样做有很多好处:通过把...
本课程中所说的组件(Component),是指一个大型系统中,某一个可以独立工作的、封装完整的组成部分。在微服务架构中,组件实际上就代表着微服务本身,或者说单个微服务。以下将其称为“单服务测试”(Single-service Test)。
这个测试的实质,就是将一个微服务与其所依赖的所有其他服务或资源全部隔离开,从该服务外部“用户”的角度来审视服务能否提供预期的输出。
这样做有很多好处:通过把测试范围限定于单个微服务,既可以对这个服务的所有行为、功能进行彻底的验收测试(Acceptance Test),同时执行速度又快得多。相对于上一课所介绍的集成测试,单服务测试的侧重点将是整个服务的功能,而不只是对外的通信与存储。
单元测试、集成测试和单服务测试(即组件测试)之间的关系如下图所示:
要把一个微服务作为一个黑盒式的测试对象,需要做到两点:
- 用模拟器来取代外部依赖;
- 可以用内部 API 终端(Endpoint)来查询或者配置微服务。
另外,通过将外部依赖、资源都模拟化,可以获得下面这些好处:
- 避免因为这些外部因素的复杂行为/不确定性,导致测试出现意外结果。
- 测试人员能够以可重复的方式,触发故障模式(Failure Mode),检查微服务在这些模式下的响应。
在具体实施这种测试时,主要有两种方法:
- 不使用网络,把所有服务和依赖模拟器都加载到同一个进程之中 (又称“单进程单服务测试”);
- 将模拟的外部依赖放在微服务的进程之外,通过真实的网络连接调用(又称“多进程单服务测试”)。
下面将着重介绍这两种方法的差异和优劣。
单进程单服务测试
单进程的意思是,把模拟器、数据库和微服务都加载到同一个进程之中,无需借助网络,如下图所示。这需要添加一个模拟的 HTTP 客户端(Stub HTTP Client)和模拟的数据库(In-Memory Datastore)。这样,就不需在测试中,通过网络访问外部服务和数据库。
这样做的好处是:加快执行速度;最大程度地减少不确定因素,降低测试的复杂度。但是其不足在于,这需要修改微服务的源代码,让其以“测试”模式运行。一般来说,依赖注入框架(Dependency Injection Frameworks)可以帮助我们做到这一点,即根据程序在启动时获得的配置,使用不同的依赖对象。
关于依赖注入框架的具体使用,请参阅这里,本课程将不赘述。目前主要的依赖注入框架包括:
- Spring
- Autofac
- Unity
在执行测试时,测试代码因为不走外部网络,所以要通过一个内部接口访问微服务,发送请求和获取响应。这通常要用一些库来进行 API 之间的转换,譬如针对 JVM 型微服务的 inproctester。
这样,就可以做到尽可能接近实际 HTTP 访问的效果,但是又不会受到实际网络交互的不确定性的影响。
另外,为了把被测微服务与外部服务隔离开,需要对服务内部的网关(Gateway)进行特别的设置,让它只使用模拟器,而不使用实际的 HTTP 客户端。这时,需要借助微服务的内部资源(Internal Resources)部分,在网关发来特定请求时,模拟器可以根据内部资源中提供的信息,返回预定的响应。
当然,模拟器也可以自己加入一些特别的测试案例(Test Case),例如:
- 外部服务连接中断;
- 外部服务的响应速度极慢;
- 外部服务的响应不正常。
这样,测试人员就可以自由地、可控地、可重复地制定多种测试案例,简化了测试的执行。
另外,用内部存储方案取代外部数据库,可以大幅度提升测试速度。当然这样做意味着无法测试实际数据库的运行情况,这需要用上一节中提到的集成测试加以弥补。
有时候,因为数据库部分的逻辑比较简单,只需要稍微加以修改,就可以满足测试需要。或者,有些数据库(例如 Cassandra 和 Elasticsearch)都提供了内嵌的部署方式。
另外,也可以采用一些工具来模拟外部数据库,例如 H2 Database Engine。
多进程单服务测试
在上面这种单进程单服务测试中,服务本身扮演着黑盒的角色。即使数据存储方式或者外部服务通信出现任何异常,测试也会顺利通过。如果采用多进程的方法,即通过实际网络调用来进行微服务与外部的交互,不仅可以考察实际网络可能造成的影响,而且不需要对微服务代码本身进行任何改动(即无需使用上面提到的“测试模式”)。而且,这时微服务需要监听某个特定的端口,在收到请求时发出响应,所以这种测试方法除了可以验证微服务的行为,还可以检查它的网络配置是否正确,能不能真正处理来自网络的请求。
不过,因为这种方式需要用外部模拟器来模拟外部服务和数据库,所以难点在于,怎么通过测试框架,有效地执行外部模拟器的启动和关闭任务,设置网络端口和配置项。测试框架必须能够在微服务启动时,将其对外部依赖资源的访问,指向正确的 URL 地址。而且,由于需要使用实际网络和实际数据库,测试执行时间很可能会延长。
那么,考虑到这些优点和缺点,什么时候应该选择这种方法呢?简单来说,如果一个微服务具有复杂的集成、存储或者启动逻辑,那么就适合使用多进程的单服务测试。
外部服务的模拟器可以采取多种形式,有些较为复杂,可以通过 API 动态设置;有些比较简单,使用固定的数据做出相应,有些采用“先记录后回访”的方式,把实际外部服务的请求和响应全部记录下来回访。可以采用的方法,包括 Moco、stubby4j 和 Mountebank 这些服务虚拟化工具(Service Virtualization Tool)。它们支持动态和固定的模拟数据,也可以使用“先记录后回访”的 VCR 方式。
在上一课的集成测试中,实际上我们也介绍了用 WireMock 这样的工具模拟外部服务的方法。那么在组件测试(单服务测试)中,区别在于所要测试的内容更加深入,不只是测试通信是否成功,而是要测试行为是否准确,响应的内容/格式是否符合预期。
以 Mountebank 为例,它可以模拟出一个虚拟的 API,供微服务调用。它支持下列协议:
- HTTP
- HTTPS
- TCP (文本和二进制)
- SMTP
安装很简单,只需要安装 Node.js v4 以上版本,就可以执行下列命令安装:
要运行 mb 服务器,执行以下命令即可:
这时,打开浏览器,访问:http://localhost:2525, 就可以看到下面的网页:
例如针对下面这段数据:
写一个简短的脚本,就能在浏览器中输入地址:http://localhost:2525/country 时返回一个列表。
在使用实际数据库时,采用正常存储和读取方法就可以。为了测试目的,可以使用 Spring,通过 profile 来切换不同的数据库。比如下面这个例子中,默认的 profile 会连接数据库 jigsaw,而名为 integration 的 profile 会连接 jigsaw_test 数据库:
针对前端微服务的组件测试
到目前为止,我们讨论的基本上都是对于后端微服务的组件测试。那么对于常见的前、后端分离的情况,怎么对前端微服务进行组件测试(单服务测试)呢?这一点采取的方法基本上和上述类似,即测试时需要模拟一个服务器,将静态内容提供给前端代码使用。这样做的好处是:
- 前后端开发相对独立;
- 后端的进度不会影响前端开发;
- 启动速度更快;
- 前后端都可以使用自己熟悉的技术栈。
但是在实际进行前后端集成时,经常会发现一些意外情况,譬如本来协商好的数据结构发生变化。这些变动因为业务的演变而在所难免,但是会花费大量的调试时间和集成时间,更别提修改之后的回归测试了。所以,仅仅使用一个静态服务器,然后提供模拟数据是远远不够的。我们需要的模拟器应该还能做到:
- 前端可以依赖指定格式的模拟数据来进行 UI 开发;
- 前端的开发和测试都基于这些模拟数据;
- 后端产生指定格式的模拟数据;
- 后端需要测试来确保生成的模拟数据正是前端需要的。
简而言之,需要在前后端之间确定一些契约(Contract),并将这些契约作为可以被测试的中间格式。然后前后端都需要有测试来使用这些契约。一旦契约发生变化,则另一方的测试会失败,这样就会驱动双方协商,并降低集成时的浪费。我们将在下一课中,介绍“契约测试”(Contract Test),它可以满足这方面的测试要求,即微服务之间的测试。
本课总结
到目前为止,通过结合单元测试、集成测试和组件测试(单服务测试),我们足以对一个微服务的所有模块,达到相当高的测试覆盖率。也就是说,如果正确地部署了这三种测试,我们应该可以发现微服务本身的大部分问题/缺陷,确保微服务的确实现了我们所需要的业务逻辑。至此,我们完成了对服务本身的各项测试,如下图所示。
但是,要让整个业务系统能够稳定工作,还必须确保不同的微服务之间,能够正确地交互和配合,这就要引入契约测试(Contract Test)的概念,我们将在下一课中详细介绍。
更多推荐
所有评论(0)