前言:
微服务遵循著单一责任 (Single Responsibility) 的设计原则, 使得微服务较传统的单体 (Monolithic) 能更容易的独立发布、部署。另一方面, 微服务能拥有更大的空间去选择适合自身的编程语言、技术。最重要的一点是, 微服务的架构更容易的能做到 “水平扩展”。
然而, 微服务也有它的技术挑战需要克服。
如图一所示, 微服务的架构是分布式的, 在这样的分布式的架构下, 所谓的 “自动化测试” 将是微服务能否成功的一个首要且关键的要素。
我们将以一系列的文章来探讨微服务自动化测试的策略、方法、工具。首先, 我们从微服务的测试策略谈起。
图一:微服务; 分布式的架构
本文:
在谈微服务的自动化测试策略前, 我们需先一起共同的来探讨: 微服务内的架构、微服务与微服务间的协作。然后, 我们就可以探讨微服务需要那些类型的自动化测试。
图二: 微服务内部的主要元素
如图二所示, 微服务的内部主要是由以下的元素所组成:
- Resources: 主要的责任是经由所选择的协议; 如: REST; 将微服务所提供的服务暴露给微服务的 Client。Resource 会完整性的校验由微服务的 Client所传过来的请求 (Request), 并且将微服务执行完某个服务后的结果, 按照由协议所界定的响应 (Response) 格式; 如: JSON; 提供给微服务的 Client。
- Service layer:主要的责任是派工给多个的 Domain, 以共同的完成微服务Client 的请求。Resource 完整的校验由微服务的 Client 所传过来的请求(Request) 后, 假如, 此请求会需要自身微服务内部或外部的微服务多个的Domain 才能完成时, Resource 便会将此请求, 交由 Service layer 来处理。Service layer 便会派工给多个的 Domain。当此请求需要调用到外部的微服务时, Service layer 便会藉由 Gateways去传递个请求 (Request) 到外部的微服务。
- Domain: 主要的责任是专注在微服务业务逻辑的处理。Domain 会将处理完业务逻辑后的结果, 传回给 Resources。
- Gateways: 主要的责任是使有关连; 会发生调用; 的微服务可以连接起来。Gateways 引导著来自自身微服务的 Service layer 的请求 (Request), 到另一个或多个的外部的微服务, 并且将外部的微服务的响应 (Response) , 传回给自身微服务的 Domain。Gateways 则是藉由 HTTP client 处理微服务间的 HTTP 协议上的请求-响应 (Request-Response)。
- Data mappers/ORM: 主要的责任是将微服务内的对象持久化。
微服务与微服务间的协作
微服务是能独立发布、独立部署的; 但, 绝不是 “孤立而行”。也就是说, 我们往往是需要多个的微服务来共同提供一具备商业价值的特性; 如图三所示。
图三: 多个微服务共同提供一具备商业价值的特性
对于由多个微服务共同提供一具备商业价值的特性的自动化测试, 我们需考量:
- 多个微服务是由一个以上的团队在负责开发时,如何确保某个团队开发的节奏、自动化的测试, 不会影响到其他团队的微服务的发布、布署? 我们将在 “能独立发布、独立部署微服务的团队阵型” 一文中再作探讨。
微服务自动化测试的类型
微服务自动化测试中的单元测试, 也是以单个或多个相关的类 (Class) 为测试的单元。
微服务自动化测试中的单元测试, 也是分成为两类:
- 社交型的单元测试 (Sociable unit testing): 主要是专注测试模块接口的行为是否正确? 而将以类 (Class) 为单元的测试, 当成是黑盒。在微服务自动化测试中, Domain往往是采用社交型的单元测试。因为, 往往我们需要多个Domain 来完成某个微服务 Client 的请求。
- 单独型的单元测试 (Solitary unit testing): 主要是专注测试单个类/ 对象和它的依赖间的行为是否正确? 我们往往会将单个类/ 对象的依赖以 Mock 或Stub 的方式来替代。在微服务自动化测试中, 我们往往会将相对独立就能完成自身任务的 Resources, Service layer, Data mappers/ORM, Gateways, HTTP client 采用单独型的单元测试。
社交型的单元测试, 单独型的单元测试对于保障 “单一个” 微服务内部的质量, 扮演著重要且关键的角色。
对于保障 “多个” 微服务间的所谓 “系统层级” 的行为正确, 我们将要藉助:
集成测试 (Integration Testing), 组件测试 (Component Testing), 契约测试 (Contract Testing), 端到端测试 (End-To-End Testing)。
- 集成测试 (Integration Testing)
在微服务自动化测试中, 所谓的集成测试 (Integration Testing) 主要是测试微服务内的模块能否与外部的微服务或外部的数据库正常的 “通信” ? 而不是在测试外部的微服务或外部的数据库。
所以, 在微服务自动化测试中的集成测试, 只需测试关键路径上成功/ 异常的场景即可。
如图四所示, 在微服务内的 Gateways/ HTTP client, Data mappers/ORM 需执行微服务自动化测试中的集成测试。
- Gateways/ HTTP client 的集成测试: 主要是测试微服务内的模块能否与外部的微服务连接, 并且检测通信协议上的问题; 如: 丢失 HTTP 的表头, 不正确的 SSL 处理, request/response 不匹配。关键的一点是: 所有错误处理的场景一定要都能被覆盖测试到。在后面的文章中, 我们也将会探讨运用 Service virtualization 测试关于 timeout 或者外部微服务延迟响应等的场景。
- Data mappers/ORM 的集成测试: 主要是测试微服务外部的数据库内的数据表结构是与微服务所期望的数据表结构是一致的。Data mappers/ORM 的集成测试, 对于 NoSQL 的数据库是相当重要、且必要的测试。
图四: 集成测试 (Integration Testing)
微服务自动化测试中的组件测试 (Component Testing), 指的是: 微服务自身的测试; 以微服务作为测试的单元。
在组件测试 (Component Testing) 中, 测试的粒度是微服务对外的API; 从微服务Client 的视角, 测试微服务对外的API 的行为是否符合预期?
在组件测试 (Component Testing) 中, 将会以 test doubles (mock/ stub) 的方式, 隔离微服务对外的依赖。
- 如图五所示, 在组件测试 (Component Testing) 中, 为了隔离微服务对外的依赖, Gateways 被配置成去调用 HTTP Client Stub; HTTP Client Stub 将会取代外部的微服务, 而对请求 (Request) 做出响应 (Response)。
- 如图五所示, 在组件测试 (Component Testing) 中, 我们会以 In-Memory 数据库; 如: H2; 取代外部数据库。这样的作法, 毫无疑问的, 将大幅的提升组件测试 (Component Testing) 执行的速度。
在后面的文章中, 我们将会探讨如何以 Arquillian 实现组件测试 (Component Testing) 。
图五: 组件测试 (Component Testing)
微服务是能独立发布、独立部署的; 但, 绝不是 “孤立而行”。也就是说, 我们往往是需要多个的微服务来共同提供一具备商业价值的特性。
当微服务是扮演提供服务的角色时, 我们便称此微服务是 “Producer”。
当微服务是扮演使用服务的角色时, 我们便称此微服务是 “Consumer”。
微服务自动化测试中的契约测试 (Contract Testing), 会在 Producer 微服务与Consumer 微服务之间, 定义一契约 (Contract)。
契约测试 (Contract Testing) 便会根据 Producer 微服务与 Consumer 微服务之间的契约, 测试 Producer 微服务与 Consumer 微服务之间的交互行为是否正确? Producer 微服务上代码的修改, 是否会影响到 Consumer 微服务, 而使 Consumer 微服务无法再运行?
如图六所示:
- Producer 微服务提供了 Resource: {id, name, age}。
- 根据 Contract A, Consumer 微服务 A, 使用了 Producer 微服务, 取得了 Resource: {id, name}。
- 根据 Contract B, Consumer 微服务 B, 使用了 Producer 微服务, 取得了 Resource: {id, age}。
- 根据 Contract C, Consumer 微服务 C, 使用了 Producer 微服务, 取得了 Resource: {id, name, age}。
在某一天, 一个新的 Consumer 微服务 Y, 要求 Producer 微服务提供: last name, first name 。
- Producer 微服务的开发人员, 便将 Producer 微服务中的 name 栏位(属性) 改成 name 类(对象); 封装著 last name, first name。
- 经由契约测试 (Contract Testing), Producer 微服务的开发人员将能立马的发现, 他(她) 在 Producer 微服务上的修改, 将会影响到 Consumer A 微服务与 Consumer C 微服务, 而使 Consumer A 微服务与 Consumer C 微服务无法再运行。
在后面的文章中, 我们将会探讨如何以 Pact 实现契约测试 (Contract Testing)。
契约测试 (Contract Testing)
- 端到端测试 (End-To-End Testing)
微服务自动化测试中的端到端测试 (End-To-End Testing), 主要是要覆盖产品中从前端 GUI 到微服务的测试。
微服务自动化测试中的端到端测试 (End-To-End Testing) 开发与维护的成本是相当的高的。
在后面的文章中, 我们将会探讨如何以 Arquillian 实现端到端测试 (End-To-End Testing)。
图七: 端到端测试 (End-To-End Testing); 从前端 GUI 到微服务的测试
测试金字塔 (Test Pyramid)
在了解了各种类型的微服务自动化测试后, 我们就可以将各种类型的微服务自动化测试, 放入测试金字塔 (Test Pyramid) 中。
如图八所示, 测试金字塔 (Test Pyramid) 可以帮助我们知道如何的去平衡 “测试成本” 与 “测试粗粒度”。
在测试金字塔 (Test Pyramid)中, 越往上升, 所代表的是: 测试的粗粒度越大, 但测试的成本 (测试执行时间) 就越高 (越长)。
所以, 在 微服务自动化测试的测试策略应该是:
- 在测试金字塔 (Test Pyramid) 中, 越往上升的测试类型, 其测试的粗粒度就越大, 而其测试的数量应递减。
- 反之, 在测试金字塔 (Test Pyramid) 中, 越往下行的测试类型, 其测试的粗粒度就越小, 而其测试的数量应递增。
- 在微服务自动化测试中, 测试数量最多的测试应该是: 单元测试 (UNIT TESTING) 。
- 在微服务自动化测试中, 测试数量最少的测试应该是: 端到端测试 (End-To-End Testing) 。
图八: 测试金字塔(Test Pyramid)
结论:
在分布式架构下的微服务, 要进行自动化测试是件相当复杂的工程。
软件工程界的巨擘; Martin Fowler; 提供了微服务测试的指引。
我们可根据 Martin Fowler 所提供的微服务测试的指引, 制订我们微服务自动化测试的测试策略。我们后续的文章, 也将会根据 Martin Fowler 所提供的微服务测试的指引, 探讨如何运用相关的测试框架、工具, 以实现微服务自动化测试。
參考資料
- Testing Strategies in a Microservice Architecture; Martin Fowler
- Microservices Patterns; Chris Richardson
- Testing Java Microservices; Alex Soto Bueno, Andy Gumbrecht, Jason Porter