契约测试之Pact By Example

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

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

 作者:ariman    来源:简书

分享:
    目前支持的匹配验证方法:
  对于Array和Map这样的数据结构,DSL也有相应匹配验证方法,我这里就不罗列了,请参考官网的介绍;
  执行Miku端的测试
  Test Case准备好后,我们就可以执行测试了。因为我们实际上是用的Junit的框架,所以和执行一般的单元测试是一样的:
 ./gradlew :example-consumer-miku:clean test
  成功执行后,你就可以在Pacts\Miku下面找到所有测试生成的契约文件。
  发布契约文件到Pact Broker
  契约文件,也就是Pacts\Miku下面的那些JSON文件,可以用来驱动Provider端的契约测试。由于我们的示例把Consumer和Provider都放在了同一个Codebase下面,所以Pacts\Miku下面的契约文件对Provider是直接可见的,而真实的项目中,往往不是这样,你需要通过某种途径把契约文件从Consumer端发送给Provider端。你可以选择把契约文件SCP到Provider的测试服务器去,也可以选择使用中间文件服务器来共享契约文件,你甚至可以直接人肉发邮件把契约文件扔给Provider的团队,然后告诉他们“这是我们的契约,你们看着办吧~”(当然,这样很Low ...),这些都是可行的。显然,Pact提供了更加优雅的方式,那就是使用Pact Broker。
  当你准备好Broker后,就可以用它来方便的实现真正的消费者驱动的契约测试了。
  好吧,我得承认,“准备”这两个字我用得有些轻描淡写,实际的情况是你可能需要费一番周折才能弄好一个Broker服务。目前有好些方法可以搭建Broker服务,你可以直接下载官网的源码然后自己折腾,也可以使用Docker来个一键了事,更可以直接找Pact官方申请一个公共的Broker,当然,那样做就得暴露你的契约给第三方服务器,真实的产品项目多半是不行的,但如果只是学习,那就事半功倍了,比如我们当前的这个示例就是如此。
  将契约文件上传到Broker服务器非常简单:
 ./gradlew :example-consumer-miku:pactPublish
  然后你会在命令行下面看到类似这样的输出:
   > Task :example-consumer-miku:pactPublish
  Publishing JunitDSLConsumer1-ExampleProvider.json ... HTTP/1.1 200 OK
  Publishing JunitDSLJsonBodyConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
  Publishing JunitDSLLambdaJsonBodyConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
  Publishing BaseConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
  Publishing JunitRuleConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
  Publishing JunitRuleMultipleInteractionsConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
  Publishing JunitDSLConsumer2-ExampleProvider.json ... HTTP/1.1 200 OK
  上传完成之后,你就可以在我们的Broker服务器上面看到对于的契约内容了。
  值得说明的是,你可以看到上面我们有7个Consumer对应1个Provdier。在真实的项目中,不应该是这样的,因为现在我们实际上只有一个Consumer Miku。我只是在不同的契约文件中对Consumer的名字做了不同的命名,目的只是展示一下Broker的这个漂亮的调用关系图。这只是一个示例,仅此而已。
  至此,Pact测试中,Consumer端的工作我们就全部搞定了,剩下的就是Provider的活了。
  Provider端的测试
  在Provider端,我们使用Gradle的Plugin来执行契约测试,非常的简单,不需要写一行测试代码:
 ./gradlew :example-provider:pactVerify
  在Provider端执行契约测试之前,我们需要先启动Provider的应用。虽然通过gradle我们可以配置自动关停应用,但对于初学者,我还是建议大家多手动捣鼓捣鼓,不然你都不知道这个测试是怎么个跑法。啥?不知道怎么启动Provider?自己去本文的开头部分找去 ...
  然后,你可以在命令行下面看到类似这样的输出:
  Arimans-MacBook-Pro:pact-jvm-example ariman$ ./gradlew :example-provider:pactVerify
  > Task :example-provider:pactVerify_ExampleProvider
  Verifying a pact between Miku - Base contract and ExampleProvider
  [Using File /Users/ariman/Workspace/Pacting/pact-jvm-example/Pacts/Miku/BaseConsumer-ExampleProvider.json]
  Given
  WARNING: State Change ignored as there is no stateChange URL
  Consumer Miku
  returns a response which
  has status code 200 (OK)
  includes headers
  "Content-Type" with value "application/json;charset=UTF-8" (OK)
  has a matching body (OK)
  Given
  WARNING: State Change ignored as there is no stateChange URL
  Pact JVM example Pact interaction
  returns a response which
  has status code 200 (OK)
  includes headers
  "Content-Type" with value "application/json;charset=UTF-8" (OK)
  has a matching body (OK)
  ...
  Verifying a pact between JunitRuleMultipleInteractionsConsumer and ExampleProvider
  [from Pact Broker https://ariman.pact.dius.com.au/pacts/provider/ExampleProvider/consumer/JunitRuleMultipleInteractionsConsumer/version/1.0.0]
  Given
  WARNING: State Change ignored as there is no stateChange URL
  Miku
  returns a response which
  has status code 200 (OK)
  includes headers
  "Content-Type" with value "application/json;charset=UTF-8" (OK)
  has a matching body (OK)
  Given
  WARNING: State Change ignored as there is no stateChange URL
  Nanoha
  returns a response which
  has status code 200 (OK)
  includes headers
  "Content-Type" with value "application/json;charset=UTF-8" (OK)
  has a matching body (OK)
   从上面的结果可以看出,我们的测试既使用了来自本地的契约文件,也使用了来自Broker的契约文件。
  由于我们示例使用的Broker服务器是公共的,任何调戏我们这个示例应用的小伙伴都能上传他们自己的契约文件,其中难免会存在错误的契约。所以,如果你发现来自Broker的契约让你的测试挂掉了,请不要惊慌哟。当然,因为是公共服务器,我会不定时的清空里面的契约文件,所以哪天你要是发现你之前上传的契约文件没有了,也不必大惊小怪。
  相关的Gradle配置
  OK,Provider和Miku感情故事我们就讲完了。在讲Nanoha之前,先让我们来看看Gradle的一些配置内容:
   project(':example-consumer-miku') {
  ...
  test {
  systemProperties['pact.rootDir'] = "$rootDir/Pacts/Miku"
  }
  pact {
  publish {
  pactDirectory = "$rootDir/Pacts/Miku"
  pactBrokerUrl = mybrokerUrl
  pactBrokerUsername = mybrokerUser
  pactBrokerPassword = mybrokerPassword
  }
  }
  ...
  }
  project(':example-consumer-nanoha') {
  ...
  test {
  systemProperties['pact.rootDir'] = "$rootDir/Pacts/Nanoha"
  }
  ...
  }
  import java.net.URL
  project(':example-provider') {
  ...
  pact {
  serviceProviders {
  ExampleProvider {
  protocol = 'http'
  host = 'localhost'
  port = 8080
  path = '/'
  // Test Pacts from local Miku
  hasPactWith('Miku - Base contract') {
  pactSource = file("$rootDir/Pacts/Miku/BaseConsumer-ExampleProvider.json")
  }
  hasPactsWith('Miku - All contracts') {
  pactFileLocation = file("$rootDir/Pacts/Miku")
  }
  // Test Pacts from Pact Broker
  hasPactsFromPactBroker(mybrokerUrl, authentication: ['Basic', mybrokerUser, mybrokerPassword])
  // Test Pacts from local Nanoha
  //                hasPactWith('Nanoha - With Nantionality') {
  //                    pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaWithNationality-ExampleProvider.json")
  //                }
  //                hasPactWith('Nanoha - No Nantionality') {
  //                    stateChangeUrl = new URL('http://localhost:8080/pactStateChange')
  //                    pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaNoNationality-ExampleProvider.json")
  //                }
  }
  }
  }
  }
  Gradle的配置也是非常的简单的,Provider,Miku和Nanoha作为三个单独的应用,都是独立配置的,其中的一些关键信息:
  systemProperties['pact.rootDir'] 指定了我们生成契约文件的路径;
  Miku中的pact { ... }定义了我们Pact Broker的服务器地址,以及我们访问时需要的认证信息。
  如果你想通过浏览器访问Broker,比如看上面的关系图,你也是需要这个认证信息的。这里的配置使用的是变量,真正的用户名和密码在哪儿?不告诉你,自己在代码里面找找吧( ̄▽ ̄)~*
  Provider的hasPactWith()和hasPactsWith()指定了执行PactVerify时会去搜索的本地路径,相应的,hasPactsFromPactBroker则是指定了Broker的服务器地址;
  为什么要注释掉Nanoha的契约文件路径呢?因为目前我们还没有生成Nanoha的契约文件,如果不注释掉它们的话,测试会报找不到文件的错误。我们可以在之后生成完Nanoha的契约文件后,再打开注释;
  Provider与Nanoha间的契约测试
  Nanoha端的契约测试和Miku端大同小异,只是我们会在Nanoha端使用ProviderState的特性。关于ProviderState的具体含义,大家可以参见官网的介绍.
  准备Provider端的ProviderState
  Provider会返回一个.nationality的字段,在真实项目里,它的值可能来自数据库(当然,也可能来自更下一层的API调用)。在我们的示例里面,简单起见,直接使用了Static的属性来模拟数据的存储:
  provider.ulti.Nationality
   public class Nationality {
  private static String nationality = "Japan";
  public static String getNationality() {
  return nationality;
  }
  public static void setNationality(String nationality) {
  Nationality.nationality = nationality;
  }
  }
  然后,通过修改.nationality就可以模拟对存储数据的修改。所以,我们定义了一个控制器pactController,在/pactStateChange上面接受POST的reqeust来修改.nationality:
  provider.PactController
   @Profile("pact")
  @RestController
  public class PactController {
  @RequestMapping(value = "/pactStateChange", method = RequestMethod.POST)
  public void providerState(@RequestBody PactState body) {
  switch (body.getState()) {
  case "No nationality":
  Nationality.setNationality(null);
  System.out.println("Pact State Change >> remove nationality ...");
  break;
  case "Default nationality":
  Nationality.setNationality("Japan");
  System.out.println("Pact Sate Change >> set default nationality ...");
  break;
  }
  }
  }
  因为这个控制器只是用来测试的,所以它应该只在非产品环境下才能可见,所以我们使用了一个pact的Profile Annotation来限制这个控制器只能在使用pact的profile时才能可见。
  OK,总结一下就是:当Provider使用pact的profile运行时,它会在URL/pactStateChange上接受一个POST请求,来修改.nationality的值,再具体一些,可以被设置成默认值Japan,或者null。
  Nanoha端的契约测试
  Nanoha端的测试文件和Miku端的差不多,我们使用Lambda DSL,在一个文件里面写两个TestCase。
   public class NationalityPactTest {
  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 testWithNationality() {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  DslPart body = newJsonBody((root) -> {
  root.numberType("salary");
  root.stringValue("fullName", "Takamachi Nanoha");
  root.stringValue("nationality", "Japan");
  root.object("contact", (contactObject) -> {
  contactObject.stringMatcher("Email", ".*@ariman.com", "takamachi.nanoha@ariman.com");
  contactObject.stringType("Phone Number", "9090940");
  });
  }).build();
  RequestResponsePact pact = ConsumerPactBuilder
  .consumer("ConsumerNanohaWithNationality")
  .hasPactWith("ExampleProvider")
  .given("")
  .uponReceiving("Query fullName is Nanoha")
  .path("/information")
  .query("fullName=Nanoha")
  .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(), "Takamachi Nanoha");
  assertEquals(information.getNationality(), "Japan");
  });
  checkResult(result);
  }
  @Test
  public void testNoNationality() {
  Map<String, String> headers = new HashMap<String, String>();
  headers.put("Content-Type", "application/json;charset=UTF-8");
  DslPart body = newJsonBody((root) -> {
  root.numberType("salary");
  root.stringValue("fullName", "Takamachi Nanoha");
  root.stringValue("nationality", null);
  root.object("contact", (contactObject) -> {
  contactObject.stringMatcher("Email", ".*@ariman.com", "takamachi.nanoha@ariman.com");
  contactObject.stringType("Phone Number", "9090940");
  });
  }).build();
  RequestResponsePact pact = ConsumerPactBuilder
  .consumer("ConsumerNanohaNoNationality")
  .hasPactWith("ExampleProvider")
  .given("No nationality")
  .uponReceiving("Query fullName is Nanoha")
  .path("/information")
  .query("fullName=Nanoha")
  .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(), "Takamachi Nanoha");
  assertEquals(information.getNationality(), null);
  });
  checkResult(result);
  }
  }
  这两个TestCase的主要区别是:
  我们对nationality的期望一个Japan,一个是null;
  通过.given()方法来指定我们的ProviderState,从而控制在Provider端运行测试之前修改对应nationality的值;
  Consumer端运行测试的方式还是一样的:
 ./gradlew :example-consumer-nanoha:clean test
  然后,就可以在Pacts\Nanoha路径下面找到生成的契约文件了。
  Provider端的契约测试
  启动Provider的应用
  上面我们提到,运行Provider需要使用pact的profile,所以现在启动Provider的命令会有所不同:
   export SPRING_PROFILES_ACTIVE=pact
  ./gradlew :example-provider:bootRun
  如果你之前已经启动了Provider,记得要kill掉哟,不然会端口占用的啦~
  修改Gradle配置文件
  我们在Consumer的契约中,使用.given()指定了ProviderState,但说到底,那里指定的只是一个字符串而已,真正干活的,还是Gradle,所以我们需要Gradle的相关配置:
   build.gralde
  hasPactWith('Nanoha - With Nantionality') {
  pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaWithNationality-ExampleProvider.json")
  }
  hasPactWith('Nanoha - No Nantionality') {
  stateChangeUrl = new URL('http://localhost:8080/pactStateChange')
  pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaNoNationality-ExampleProvider.json")
  }
  这里,我们取消了之前对Nanoha的注释。第一个TestCase我们会测试使用默认的nationality=Japan。第二个TestCase,我们指定了stateChangeUrl,它会保证在测试运行之前,先发送一个POST请求给这个URL,然后我们的TestCase测试nationality=null。
  执行契约测试
  同样的方法执行契约测试:
 ./gradlew :example-provider:pactVerify
  然后你就可以在命令行下面看见对应的输出了。
  验证我们的测试
  如果你一字不漏的玩儿到了这里,那么恭喜你,你应该可以在自己的项目里去实践Pact了(好了,那个抄椅子的同学,你不用说了,我知道,你们用的是Python╮(╯_╰)╭)。
  但是在离开本示例之前,还是发扬一下我们的测试精神吧,比如,搞点小破坏~
  在Provider返回的body里面,Miku和Nanoha都有使用字段.name。如果某天,Provider想把.name改成.fullname,估计Miku和Nanoha就要跪了。这是一种经典的契约破坏场景,用来做我们的玩儿法再适合不过了。可是要那么玩儿的话,需要修改Provider的好些代码,想必不少测试的同学,特别是对Spring Boot不了解的同学就又要拍砖了。
  所以还是让我们来个简单的吧,比如霸王硬上弓,直接把.name给miku了,哦,不对,是null了。
  provider.InformationController
  @RestController
  public class InformationController {
  ...
  information.setName(null);
  return information;
  }
  }
  喂,喂,干坐着干嘛,动手改呀!这行代码可是需要你们自己加上去的哟,即便它已经简单到只有一行。然后,那个写Python的,别告诉我你看不懂information.setName(null),Okay? ̄▽ ̄
  最后,重新运行我们的契约测试,你就能看到一些长得像这样的东东啦~:
   ...
  Verifying a pact between Nanoha - No Nantionality and ExampleProvider
  [Using File /Users/ariman/Workspace/Pacting/pact-jvm-example/Pacts/Nanoha/ConsumerNanohaNoNationality-ExampleProvider.json]
  Given No nationality
  Query name is Nanoha
  returns a response which
  has status code 200 (OK)
  includes headers
  "Content-Type" with value "application/json;charset=UTF-8" (OK)
  has a matching body (FAILED)
  Failures:
  0) Verifying a pact between Miku - Base contract and ExampleProvider - Pact JVM example Pact interactionVerifying a pact between Miku - Base contract and ExampleProvider - Pact JVM example Pact interaction Given  returns a response which has a matching body
  $.name -> Expected 'Hatsune Miku' but received null
  ...
  
     上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
22/2<12
精选软件测试好文,快来阅读吧~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号