class: center, middle # 基于PACT的微服务契约测试 张桐 2017年4月23日 --- # About me * 现在华为公司2012实验室中软架构设计与管理部 * 帮助团队进行微服务/持续交付/DevOps等方面的持续改进 * 曾任ThoughtWorks高级咨询师、网易电子商务部高级工程师 * 10年IT与互联网行业经验 --- # Agenda * 微服务集成测试之痛 * 消费者驱动的契约测试 * PACT测试实践 * 总结 --- # 微服务架构 * Martin Fowler: [Microservices](https://martinfowler.com/articles/microservices.html) * 单一应用被划分成一组**小**的服务 * 运行在单**独**的进程中 * **轻**量级通信机制 * **松**耦合,可独立部署、独立运行 .right[![](images/honey_comb.png)] --- # 微服务架构下的新问题 * 每个微服务可能由不同团队维护,怎样高效沟通和协作,保持服务接口的稳定性和正确性? * 消费者端的服务如何将自己对接口需求的变化告知服务提供者? * 提供者端的服务如何保证自己的接口实现发生变动时,不影响服务消费者? * 团队成员如何了解众多服务之间的具体调用关系? .center[![](images/tech.png)] --- # 集成测试 * An integration test verifies the communication paths and interactions between components to detect interface defects. > Toby Clemson: [Testing Strategies in a Microservice Architecture](https://martinfowler.com/articles/microservice-testing/#testing-integration-introduction) * 集成测试:通过验证组件间的通信路径和交互来检测接口的质量缺陷 .center[![](images/gears.png)] --- # 集成测试方法 * 通常我们这样做集成测试 .center[![](images/integration_test.png)] --- # 微服务集成测试之痛 * 环境搭建成本高,需启动多个服务 * 学习成本高 * 用例编写难 * 运行慢 * 发现问题晚 * 测试脆弱,外部依赖多 .right[![](images/elephant.png)] --- # 消费者驱动的契约测试——概念 * 契约 * 以结构化文档(json或yaml等)形式对服务接口的约定 * 问题来了 * 如何保证契约和实现的一致性? * 提供者端如何在修改了实现后,保证不影响消费者端的使用? * 契约测试 * 以契约作为唯一耦合点,验证提供者端的实现是否满足消费者端需求的过程 * 消费者驱动的契约测试 * 从消费者端的期望出发定义契约,并以契约驱动提供者端测试的过程 --- # 消费者驱动的契约测试——步骤 .width-40.float-left[ * 步骤1:消费者端 * 编写并运行单元测试(包括对接口的请求参数和预期响应) * MockService代替实际服务提供者(自动) * 生成契约文件(自动) * 步骤2:提供者端 * 启动服务提供者 * 重放契约文件中的请求,验证真实响应是否满足预期(自动) ] .float-right[![](images/pact_two_parts.png)] --- # 消费者驱动的契约测试——价值 * 将一个笨重的集成测试化为两个容易编写、容易运行的单元测试/接口测试 * 解耦消费者与提供者,甚至可以在没有提供者实现的情况下开展消费者端测试 * 通过测试保证契约和实现的一致性,测试通过之时就是代码实现完成之时 * 测试前移:在开发阶段就应该运行,并作为CI的一部分,便于尽早发现问题 * 工具/框架: * [PACT](https://github.com/realestate-com-au/pact)(推荐) * 轻量级、支持多种语言应用、支持与Maven/Gradle等集成 * [Spring Cloud Contract](https://cloud.spring.io/spring-cloud-contract/) * 支持基于JVM的应用、支持与Spring其它组件集成 .right[![](images/squirrel.png)] --- # 使用PACT实现契约测试——例子 * 待测接口 * Provider Service -> Consumer Service * GET /foos * Response * status: 200 * body: [ { "value": 42 }, { "value": 100 } ] --- # 使用PACT实现契约测试——例子 * 消费者端:使用pact-jvm-consumer-junit * 编写单元测试,使用客户端发送http请求;运行单元测试 `mvn test` * 服务提供者以MockService代替(PACT实现) ```java @Pact(provider="ProviderService", consumer="ConsumerService") public PactFragment createFragment(PactDslWithProvider builder) { Map
headers = new HashMap<>(); headers.put("Content-Type", "application/json;charset=UTF-8"); return builder.given("some state") .uponReceiving("a request for Foos") .path("/foos") .method("GET") .willRespondWith() .headers(headers) .status(200) .body("[{\"value\":42}, {\"value\":100}]").toFragment(); } @Test @PactVerification("ProviderService") public void runTest() { assertEquals(new ConsumerClient(rule.getConfig().url()).foos(), Arrays.asList(new Foo(42), new Foo(100))); } ``` --- # 使用PACT实现契约测试——例子 * 契约文件(由PACT自动生成) * 以json形式对接口形式作出约定 ```json { "provider": { "name": "ProviderService" }, "consumer": { "name": "ConsumerService" }, "interactions": [ { "description": "a request for Foos", "request": { "method": "GET", "path": "/foos" }, "response": { "status": 200, "headers": { "Content-Type": "application/json;charset=UTF-8" }, "body": [ { "value": 42 }, { "value": 100 } ] }, "providerState": "some state" } ] } ``` --- # 使用PACT实现契约测试——例子 * 提供者端:支持多语言、多种插件,这里使用pact-jvm-provider-maven * 启动服务提供者 `mvn spring-boot:run` * 运行 `mvn pact:verify` 进行测试 ```xml
au.com.dius
pact-jvm-provider-maven_2.11
3.3.6
ProviderService
http
localhost
8080
/
../pact-consumer/target/pacts/
``` --- # 预置提供者状态——Provider State * Provider State:提供者的某种状态,如预置了测试数据、模拟了异常等 * 作用:赋予对同一接口的不同用例/场景的测试能力 * 消费者端指定state: ```java builder.given("some state") ``` * 契约文件中: ```json "providerState": "some state" ``` * 提供者端通过state入口(对应的用例执行前)对提供者状态进行修改: ```java @State("some state") public void toSomeState() { // Prepare service before interaction that require "some" state // ... System.out.println("Now service in some state"); } ``` --- # 契约文件的共享方法 * 怎样在消费者和提供者之间共享契约文件? * A. 手动Copy或公共目录(简单,适于本地运行) * B. 使用[Pact Broker](https://github.com/bethesque/pact_broker)作为中间服务(推荐用于CI):可与CI集成完全自动化、可管理版本、API可视化、可自动生成服务调用关系图 .float-left.left[![](images/autogenerated_documentation.png)] .float-right.right[![](images/network_diagram.png)] --- # 使用Pact Broker实现契约管理 * 搭建Pact Broker: * 推荐使用[Dockerised Pact Broker](https://hub.docker.com/r/dius/pact_broker/) + [Docker Compose](https://docs.docker.com/compose/)实现一条命令式安装 * 使用Pact Broker: 1. 消费者端的CI build通过后,将pact文件push到pact broker上 2. 提供者端的CI build时,从pact broker上取pact文件进行测试 .right[![](images/how_pact_broker_works.png)] --- # FAQ * .blueviolet[**PACT支持哪些语言的应用?**] * 消费者端:Java/Ruby/Go/.Net/Scala/Groovy/JavaScript等 * 提供者端:提供Java/Ruby/Go/.Net等DSL支持,其他语言的应用使用pact-provider-proxy后可以与语言无关 * .blueviolet[**PACT适合做哪些测试?不适合做哪些测试?**] * 适合做服务之间接口形式的验证 * 不适合做复杂逻辑的测试、专项测试(性能、安全) * .blueviolet[**消费者端的测试应该是什么类型/级别?**] * 优先使用单元测试(只测试负责发送http调用请求的部分),更容易运行、更稳定 * .blueviolet[**提供者端的测试中是否需要Mock数据库部分?**] * 为了测试与真实环境类似的完整环境,尽量不Mock数据库,但如果底层系统过于庞大而复杂,或者需要模拟异常情况,也可以Mock一部分底层系统 * .blueviolet[**怎样实现对预期数据的模糊匹配?**] * PACT提供了灵活而全面的DSL,如正则表达式匹配、类型匹配等能力 --- # 总结 * .red[**消费者驱动的契约测试**]:以服务消费者的期望为契约,对服务提供者的交互接口进行验证的一种轻量级自动化测试方法 * 集成测试 v.s. 消费者驱动的契约测试 .left.indent[ |集成测试|消费者驱动的契约测试| |------------|------------------------------| |大而重|小而轻| |学习成本较高|学习成本较低| |从系统集成角度去验证|从服务交互角度去验证| |需要完整的集成环境|在本地就可以运行| |不易与CI集成|容易与CI集成| |测试人员负责维护|开发和测试人员共同维护| |到测试阶段才能发现问题|在开发阶段就可以发现问题| ] --- # 总结 * 推荐实践步骤 1. 梳理服务接口 2. 搭建pact broker 3. 编写消费者端和服务者端测试,并放到CI上 * 注意 * 提交代码前,要预先运行本地的pact测试进行验证 * 消费者端的CI build结束时,应自动触发提供者端的CI build --- # 附录 #### 参考资料 * [Pact](https://github.com/realestate-com-au/pact) * [pact-jvm](https://github.com/DiUS/pact-jvm) * [Pact Broker](https://github.com/bethesque/pact_broker) * [Consumer Driven Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html) - Ian Robinson * [Integration Contract Tests](https://martinfowler.com/bliki/IntegrationContractTest.html) - Martin Fowler #### 例程代码 * [A minimum pact jvm demo](https://github.com/tongzh/pact-jvm-minimum-demo) --- class: center, middle # THANK YOU Slideshow created using [remark](http://github.com/gnab/remark).