契约测试之Pact By Example

发表于:2019-6-12 14:40

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:ariman    来源:简书

  如今,契约测试已经逐渐成为测试圈中一个炙手可热的话题,特别是在微服务大行其道的行业背景下,越来越多的团队开始关注服务之间的契约及其契约测试。
  从2015年开始我就在Thoughtworks和QA Community里推广契约测试,收到了不错的成效,期间有不少同学和我讨论过如何上手契约测试,发现网上介绍契约测试的讲义、博客不乏其数(当然,质量也参差不齐),可手把手教你写契约测试的文章却几乎没有,原因恐怕就是契约测试的特性吧。契约测试是架设在消费者和服务者之间的一种比较特殊的测试活动,如果你只是想自己学习而又没有合适的项目环境,那你得自己先准备适当的消费者和服务者程序源代码,然后再开始写契约测试,而不是像写个Selenium测试那样,两三行代码就可以随随便便地调戏度娘。
  所以,我花了些时间磨叽出了这片文章……
  本文不会涉及契约测试的基本概念,因为相应的文章网上太多了,请大家自己去捞吧。本文也不会讨论契约测试的使用场景以及对其的正确理解,这方面的话题我会在今后另文介绍。
  OK,以下开始正文!
  契约测试的精髓在于消费者驱动,实践消费者驱动契约测试的工具主要有Pact,Pacto 和 Spring Cloud Contract,其中Pact是目前最为推荐的,我下面的例子都将使用Pact来练习。Pact最早是用Ruby实现的,目前已经扩展支撑Java,.NET,Javascript,Go,Swift,Python和PHP,其中Java(JVM)是我们目前项目中使用最频繁的,所以我的例子亦都是基于PACT JVM来实现(观众A:我们都用Pyhton,你丫给我说Java(╯°□°)╯︵┻━┻)
  示例源码
  大家可以从Github上获取本文示例的源码,也可以从PACT JVM官网上面找到对应的PACT-JVM-Example的链接
  示例应用
  示例应用非常简单,一个服务提供者Provider,两个服务消费者Miku和Nanoha(啥?你不知道Miku和Nanoha是什么?......问度娘吧......(~o ̄3 ̄)~......)。
  Provider
  Provider是一个简单的API,返回一些个人信息。
  启动Provider:
 ./gradlew :example-provider:bootRun
  然后访问 http://localhost:8080/information?name=Miku
  或者访问 http://localhost:8080/information?name=Nanoha
  消费者 Miku & Nanoha
  Miku和Nanoha调用Provider的API拿到各自的数据,然后显示在前端页面上。
  启动Miku:
 ./gradlew :example-consumer-miku:bootRun
  然后用浏览器访问 http://localhost:8081/miku
  启动Nanoha:
 ./gradlew :example-consumer-nanoha:bootRun
  然后用浏览器访问 http://localhost:8082/nanoha
  Miku和Nanoha做的事情基本一样,差别就是Nanoha会去多拿.nationality这个字段,而.salary这个字段Miku和Nanoha都没有用到。
  示例中的1个Provider和2个Consumer都在一个codebase里面,这只是为了方便管理示例代码,而实际的项目中,绝大多数的Provider和Consumer都是在不同的codebase里面管理的,请注意哟!
  Provider与Miku间的契约测试
  好了,大概了解示例应用之后,我们就可以开始写契约测试了(当然,如果你还想再撩撩示例的源码,也是可以的啦,不过相信我,里面没多少油水的)
  我们先从Provider和Miku之间的契约测试开始。
  请注意"之间"这个关键词,当我们谈论契约测试时,一定要明确它是建立在某一对Provider和Consumer之间的测试活动。没有Provider,Consumer做不了契约测试;没有Consumer,Provider不需要做契约测试。
  编写消费者Miku端的测试案例
  目前,PACT JVM在消费者端的契约测试主要有三种写法:
  基本的Junit
  “talk is cheap, show you the code”
  PactBaseConsumerTest.java
   public class PactBaseConsumerTest extends ConsumerPactTestMk2 {
  @Override
  @Pact(provider="ExampleProvider", consumer="BaseConsumer")
  public RequestResponsePact createPact(PactDslWithProvider builder) {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  return builder
  .given("")
  .uponReceiving("Pact JVM example Pact interaction")
  .path("/information")
  .query("fullName=Miku")
  .method("GET")
  .willRespondWith()
  .headers(headers)
  .status(200)
  .body("{\n" +
  "    \"salary\": 45000,\n" +
  "    \"fullName\": \"Hatsune Miku\",\n" +
  "    \"nationality\": \"Japan\",\n" +
  "    \"contact\": {\n" +
  "        \"Email\": \"hatsune.miku@ariman.com\",\n" +
  "        \"Phone Number\": \"9090950\"\n" +
  "    }\n" +
  "}")
  .toPact();
  }
  @Override
  protected String providerName() {
  return "ExampleProvider";
  }
  @Override
  protected String consumerName() {
  return "BaseConsumer";
  }
  @Override
  protected void runTest(MockServer mockServer) throws IOException {
  ProviderHandler providerHandler = new ProviderHandler();
  providerHandler.setBackendURL(mockServer.getUrl());
  Information information = providerHandler.getInformation();
  assertEquals(information.getName(), "Hatsune Miku");
  }
  }
  这里的关键是createPact和runTest这两个方法:
  createPact直接定义了契约交互的全部内容,比如Request的路径和参数,以及返回的Response的具体内容;
  runTest是执行测试的方法,其中ProviderHandler是Miku应用代码中的类,我们直接使用它来发送真正的Request,发给谁呢?发给mockServer,Pact会启动一个mockServer, 基于Java原生的HttpServer封装,用来代替真正的Provider应答createPact中定义好的响应内容,继而模拟了整个契约的内容;
  runTest中的断言可以用来保证我们编写的契约内容是符合Miku期望的,你可以把它理解为一种类似Consumer端的集成测试;
  Junit Rule
  PactJunitRuleTest.java
   public class PactJunitRuleTest {
  @Rule
  public PactProviderRuleMk2 mockProvider = new PactProviderRuleMk2("ExampleProvider",this);
  @Pact(consumer="JunitRuleConsumer")
  public RequestResponsePact createPact(PactDslWithProvider builder) {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  return builder
  .given("")
  .uponReceiving("Pact JVM example Pact interaction")
  .path("/information")
  .query("fullName=Miku")
  .method("GET")
  .willRespondWith()
  .headers(headers)
  .status(200)
  .body("{\n" +
  "    \"salary\": 45000,\n" +
  "    \"fullName\": \"Hatsune Miku\",\n" +
  "    \"nationality\": \"Japan\",\n" +
  "    \"contact\": {\n" +
  "        \"Email\": \"hatsune.miku@ariman.com\",\n" +
  "        \"Phone Number\": \"9090950\"\n" +
  "    }\n" +
  "}")
  .toPact();
  }
  @Test
  @PactVerification
  public void runTest() {
  ProviderHandler providerHandler = new ProviderHandler();
  providerHandler.setBackendURL(mockProvider.getUrl());
  Information information = providerHandler.getInformation();
  assertEquals(information.getName(), "Hatsune Miku");
  }
  }
  相较于基本的Junit写法,PactProviderRuleMk2能够让代码更加的简洁,它还可以自定义Mock Provider的address和port。如果像上面的代码一样省略address和port,则会默认使用127.0.0.1和随机端口。Junit Rule还提供了方法让你可以同时对多个Provider进行测试,以及让Mock Provider使用HTTPS进行交互。
  基于体力有限,本示例没有包含MultiProviders和HTTPS的例子,有需要的同学可以在PACT JVM官网上查询相关的用法......别,别打呀,俺承认,俺就是懒...还打...#$%&*!@#$%&...喂:110吗?俺要报警......
  Junit DSL
  PactJunitDSLTest
   public class PactJunitDSLTest {
  private void checkResult(PactVerificationResult result) {
  if (result instanceof PactVerificationResult.Error) {
  throw new RuntimeException(((PactVerificationResult.Error)result).getError());
  }
  assertEquals(PactVerificationResult.Ok.INSTANCE, result);
  }
  @Test
  public void testPact1() {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  RequestResponsePact pact = ConsumerPactBuilder
  .consumer("JunitDSLConsumer1")
  .hasPactWith("ExampleProvider")
  .given("")
  .uponReceiving("Query fullName is Miku")
  .path("/information")
  .query("fullName=Miku")
  .method("GET")
  .willRespondWith()
  .headers(headers)
  .status(200)
  .body("{\n" +
  "    \"salary\": 45000,\n" +
  "    \"fullName\": \"Hatsune Miku\",\n" +
  "    \"nationality\": \"Japan\",\n" +
  "    \"contact\": {\n" +
  "        \"Email\": \"hatsune.miku@ariman.com\",\n" +
  "        \"Phone Number\": \"9090950\"\n" +
  "    }\n" +
  "}")
  .toPact();
  MockProviderConfig config = MockProviderConfig.createDefault();
  PactVerificationResult result = runConsumerTest(pact, config, mockServer -> {
  ProviderHandler providerHandler = new ProviderHandler();
  providerHandler.setBackendURL(mockServer.getUrl(), "Miku");
  Information information = providerHandler.getInformation();
  assertEquals(information.getName(), "Hatsune Miku");
  });
  checkResult(result);
  }
  @Test
  public void testPact2() {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  RequestResponsePact pact = ConsumerPactBuilder
  .consumer("JunitDSLConsumer2")
  .hasPactWith("ExampleProvider")
  .given("")
  .uponReceiving("Query fullName is Nanoha")
  .path("/information")
  .query("fullName=Nanoha")
  .method("GET")
  .willRespondWith()
  .headers(headers)
  .status(200)
  .body("{\n" +
  "    \"salary\": 80000,\n" +
  "    \"fullName\": \"Takamachi Nanoha\",\n" +
  "    \"nationality\": \"Japan\",\n" +
  "    \"contact\": {\n" +
  "        \"Email\": \"takamachi.nanoha@ariman.com\",\n" +
  "        \"Phone Number\": \"9090940\"\n" +
  "    }\n" +
  "}")
  .toPact();
  MockProviderConfig config = MockProviderConfig.createDefault();
  PactVerificationResult result = runConsumerTest(pact, config, mockServer -> {
  ProviderHandler providerHandler = new ProviderHandler();
  providerHandler.setBackendURL(mockServer.getUrl(), "Nanoha");
  Information information = providerHandler.getInformation();
  assertEquals(information.getName(), "Takamachi Nanoha");
  });
  checkResult(result);
  }
  }
  基本的Junit和Junit Rule的写法只能在一个测试文件里面写一个Test Case,而使用Junit DSL则可以像上面的例子一样写多个Test Case。同样,你也可以通过MockProviderConfig.createDefault()配置Mock Server的address和port。上面的例子使用了默认配置。
  PactJunitDSLJsonBodyTest
   public class PactJunitDSLJsonBodyTest {
  PactSpecVersion pactSpecVersion;
  private void checkResult(PactVerificationResult result) {
  if (result instanceof PactVerificationResult.Error) {
  throw new RuntimeException(((PactVerificationResult.Error)result).getError());
  }
  assertEquals(PactVerificationResult.Ok.INSTANCE, result);
  }
  @Test
  public void testWithPactDSLJsonBody() {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  DslPart body = new PactDslJsonBody()
  .numberType("salary", 45000)
  .stringType("fullName", "Hatsune Miku")
  .stringType("nationality", "Japan")
  .object("contact")
  .stringValue("Email", "hatsune.miku@ariman.com")
  .stringValue("Phone Number", "9090950")
  .closeObject();
  RequestResponsePact pact = ConsumerPactBuilder
  .consumer("JunitDSLJsonBodyConsumer")
  .hasPactWith("ExampleProvider")
  .given("")
  .uponReceiving("Query fullName is Miku")
  .path("/information")
  .query("fullName=Miku")
  .method("GET")
  .willRespondWith()
  .headers(headers)
  .status(200)
  .body(body)
  .toPact();
  MockProviderConfig config = MockProviderConfig.createDefault(this.pactSpecVersion.V3);
  PactVerificationResult result = runConsumerTest(pact, config, mockServer -> {
  ProviderHandler providerHandler = new ProviderHandler();
  providerHandler.setBackendURL(mockServer.getUrl());
  Information information = providerHandler.getInformation();
  assertEquals(information.getName(), "Hatsune Miku");
  });
  checkResult(result);
  }
  @Test
  public void testWithLambdaDSLJsonBody() {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  DslPart body = newJsonBody((root) -> {
  root.numberValue("salary", 45000);
  root.stringValue("fullName", "Hatsune Miku");
  root.stringValue("nationality", "Japan");
  root.object("contact", (contactObject) -> {
  contactObject.stringMatcher("Email", ".*@ariman.com", "hatsune.miku@ariman.com");
  contactObject.stringType("Phone Number", "9090950");
  });
  }).build();
  RequestResponsePact pact = ConsumerPactBuilder
  .consumer("JunitDSLLambdaJsonBodyConsumer")
  .hasPactWith("ExampleProvider")
  .given("")
  .uponReceiving("Query fullName is Miku")
  .path("/information")
  .query("fullName=Miku")
  .method("GET")
  .willRespondWith()
  .headers(headers)
  .status(200)
  .body(body)
  .toPact();
  MockProviderConfig config = MockProviderConfig.createDefault(this.pactSpecVersion.V3);
  PactVerificationResult result = runConsumerTest(pact, config, mockServer -> {
  ProviderHandler providerHandler = new ProviderHandler();
  providerHandler.setBackendURL(mockServer.getUrl());
  Information information = providerHandler.getInformation();
  assertEquals(information.getName(), "Hatsune Miku");
  });
  checkResult(result);
  }
  }
  当然,Junit DSL的强大之处绝不仅仅是让你多写几个Test Case, 通过使用PactDslJsonBody和Lambda DSL你可以更好的编写你的契约测试文件:
  对契约中Response Body的内容,使用JsonBody代替简单的字符串,可以让你的代码易读性更好;
  JsonBody提供了强大的Check By Type和Check By Value的功能,让你可以控制对Provider的Response测试精度。比如,对于契约中的某个字段,你是要确保Provider的返回必须是具体某个数值(check by Value),还是只要数据类型相同就可以(check by type),比如都是String或者Int。你甚至可以直接使用正则表达式来做更加灵活的验证;
  
     上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号