82. Spring Cloud Contract 常见问题解答
82.1 为什么要使用 Spring Cloud Contract Verifier 而不是 X?
对于 time 是 Spring Cloud Contract Verifier 是一个基于 JVM 的工具。因此,当您已经为 JVM 创建软件时,它可能是您的首选。这个项目有很多非常有趣的 features,但特别是其中一些肯定会让 Spring Cloud Contract Verifier 在 Consumer Driven Contract(CDC)工具的“市场”中脱颖而出。在许多最有趣的是:
-
可以通过消息传递进行 CDC
-
清晰易用,静态类型的 DSL
-
可以将当前 JSON 文件粘贴到 contract 并仅编辑其元素
-
从定义的 Contract 自动生成测试
-
Stub Runner 功能 - 存根在运行时从 Nexus/Artifactory 自动下载
-
Spring Cloud integration - integration 测试不需要发现服务
82.2 我不想在 Groovy 中写一个 contract!
没问题。你可以在 YAML 写一个 contract!
82.3 这是 value(consumer(),producer())是什么?
与存根相关的最大挑战之一是它们的可重用性。只有当它们被广泛使用时,它们才会达到目的。通常使这种困难的是请求/响应元素的 hard-coded 值。对于 example 日期或 ID。想象一下以下 JSON 请求
{
"time" : "2016-10-10 20:10:15",
"id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
"body" : "foo"
}
和 JSON 响应
{
"time" : "2016-10-10 21:10:15",
"id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
"body" : "bar"
}
想象一下,通过更改系统中的时钟或提供数据提供程序的 stub implementations,设置time
字段的正确 value 所需的痛苦(让我们假设这个内容是由数据库生成的)。这与称为id
的字段有关。你会创建一个 UUID generator 的 stubbed implementation 吗?没什么意义......
因此,作为 consumer,您希望发送与任何形式的 time 或任何 UUID 匹配的请求。这样你的系统将照常工作 - 将生成数据,你不必存根。让我们假设在上述 JSON 的情况下,最重要的部分是body
字段。您可以专注于此并为其他字段提供匹配。换句话说,你希望存根像这样工作:
{
"time" : "SOMETHING THAT MATCHES TIME",
"id" : "SOMETHING THAT MATCHES UUID",
"body" : "foo"
}
对于作为 consumer 的响应,您需要一个可以操作的具体 value。所以这样的 JSON 是有效
{
"time" : "2016-10-10 21:10:15",
"id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
"body" : "bar"
}
正如您在前面部分中看到的,我们从 contracts 生成测试。所以从生产者的角度来看情况看起来很不一样。我们正在解析提供的 contract,在测试中我们要向您的 endpoints 发送一个真实的请求。因此,对于请求的 producer,我们不能进行任何匹配。我们需要 producer 的后端可以使用的具体值。这样的 JSON 是有效的:
{
"time" : "2016-10-10 20:10:15",
"id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
"body" : "foo"
}
另一方面,从 contract 的有效性的角度来看,响应不一定必须包含time
或id
的具体值。假设您在 producer 端生成了那些 - 再次,您必须进行大量的存根以确保始终 return 相同的值。这就是为什么从 producer 的方面你可能想要的是以下响应:
{
"time" : "SOMETHING THAT MATCHES TIME",
"id" : "SOMETHING THAT MATCHES UUID",
"body" : "bar"
}
那你怎么能为 consumer 提供一个 time 匹配器,为 producer 提供一个具体的 value,反之亦然?在 Spring Cloud Contract 中,我们允许您提供动态 value。这意味着通信双方可能会有所不同。您可以传递值:
通过value
方法
value(consumer(...), producer(...))
value(stub(...), test(...))
value(client(...), server(...))
或使用$()
方法
$(consumer(...), producer(...))
$(stub(...), test(...))
$(client(...), server(...))
您可以在Contract DSL 部分中阅读更多相关信息。
调用value()
或$()
告诉 Spring Cloud Contract 您将传递动态 value。在consumer()
方法中,您传递应该在 consumer 端(在生成的存根中)使用的 value。在producer()
方法中,您传递应该在 producer 端使用的 value(在生成的测试中)。
如果一方面你已经传递了正则表达式并且你没有通过另一方,那么另一方将获得 auto-generated。
通常,您将使用该方法和regex
帮助器方法。 E.g。 consumer(regex('[0-9]{10}'))
。
总结一下,前面提到的场景的 contract 看起来或多或少会像这样(time 和 UUID 的正则表达式被简化,很可能无效但我们希望在这个例子中保持简单):
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
url '/someUrl'
body([
time : value(consumer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
id: value(consumer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
body: "foo"
])
}
response {
status 200
body([
time : value(producer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
id: value([producer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
body: "bar"
])
}
}
请阅读与 JSON 相关的 Groovy docs以了解如何正确构建请求/响应主体。
82.4 如何进行 Stubs 版本控制?
82.4.1 API 版本控制
让我们试着回答一个问题版本真正意味着什么。如果您指的是 API version,那么有不同的方法。
-
使用超媒体,链接,不要以任何方式 version 你的 API
-
通过 headers/urls 传递版本
我不会试图回答哪个方法更好的问题。无论什么适合您的需求,并允许您生成业务 value 应该被选中。
我们假设你做了 version 你的 API。在这种情况下,您应该提供与您支持的许多版本一样多的 contracts。您可以为每个 version 创建一个子文件夹,或者将其附加到 contract name - 任何适合您的内容。
82.4.2 JAR 版本控制
如果版本控制是指包含存根的 JAR 的 version,那么基本上有两种主要方法。
让我们假设您正在进行持续交付/部署,这意味着您在每次_tar 通过管道时生成 jar 的新 version,并且 jar 可以在任何 time 时转到 production。对于 example,jar version 看起来像这样(它建立在 20:15:21 的 20.10.2016 上):
1.0.0.20161020-201521-RELEASE
在这种情况下,生成的 stub jar 将如下所示。
1.0.0.20161020-201521-RELEASE-stubs.jar
在这种情况下,当引用存根提供存根的最新 version 时,您应该在application.yml
或@AutoConfigureStubRunner
内。你可以通过传递+
符号来做到这一点。 例
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})
如果版本控制是固定的(e.g. 1.0.4.RELEASE
或2.1.1
),那么你必须设置 jar version 的具体 value。 例如 2.1.1.
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:2.1.1:stubs:8080"})
82.4.3 Dev 或 prod 存根
您可以操作分类器以针对其他服务的存根或部署到 production 的存根的当前开发 version 运行测试。如果在到达 production 部署后更改 build 以使用prod-stubs
分类器部署存根,则可以在一个情况下使用 dev 存根和一个带有 prod 存根的测试运行测试。
使用存根的开发 version 的测试示例
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})
使用 production version 存根的测试示例
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:prod-stubs:8080"})
您也可以通过部署管道中的 properties 传递这些值。
82.5 Common repo with contracts
存储 contracts 而不是将它们与 producer 一起存储的另一种方法是将它们保存在 common 位置。它可能与消费者无法克隆 producer 的 code 的安全问题有关。此外,如果您将 contracts 保留在一个地方,那么作为一个 producer,您将知道您拥有多少消费者以及哪些消费者将根据您的本地更改进行 break。
82.5.1 Repo 结构
假设我们有一个带有坐标com.example:server
和 3 个消费者的 producer:client1
,client2
,client3
。然后在带有 common contracts 的 repository 中,您将进行以下设置(您可以将其签出这里:
├── com
│ └── example
│ └── server
│ ├── client1
│ │ └── expectation.groovy
│ ├── client2
│ │ └── expectation.groovy
│ ├── client3
│ │ └── expectation.groovy
│ └── pom.xml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
└── assembly
└── contracts.xml
正如您可以看到 slash-delimited groupid /
artifact id 文件夹(com/example/server
)下的 3 个消费者(client1
,client2
和client3
)的期望。期望是本文档中描述的标准 Groovy DSL contract files。这个 repository 必须生成一个 JAR 文件,该文件与 repo 的内容一一对应。
_ server
文件夹中的pom.xml
示例。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Server Stubs</name>
<description>POM used to install locally stubs for consumer side</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.16.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<spring-cloud-contract.version>1.2.7.BUILD-SNAPSHOT</spring-cloud-contract.version>
<spring-cloud-dependencies.version>1.3.11.BUILD-SNAPSHOT</spring-cloud-dependencies.version>
<excludeBuildFolders>true</excludeBuildFolders>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- By default it would search under src/test/resources/ -->
<contractsDirectory>${project.basedir}</contractsDirectory>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
正如您所看到的,除了 Spring Cloud Contract Maven 插件之外,没有任何依赖项。那些 poms 是 consumer 方面_jun mvn clean install -DskipTests
本地安装 producer 项目的存根所必需的。
根文件夹中的pom.xml
可以如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.standalone</groupId>
<artifactId>contracts</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Contracts</name>
<description>Contains all the Spring Cloud Contracts, well, contracts. JAR used by the producers to generate tests and stubs</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>contracts</id>
<phase>prepare-package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<attach>true</attach>
<descriptor>${basedir}/src/assembly/contracts.xml</descriptor>
<!-- If you want an explicit classifier remove the following line -->
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
它使用 order 中的程序集插件来使用 contracts build JAR。 此类设置的示例如下:
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>project</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>/</outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<excludes>
<exclude>**/${project.build.directory}/**</exclude>
<exclude>mvnw</exclude>
<exclude>mvnw.cmd</exclude>
<exclude>.mvn/**</exclude>
<exclude>src/**</exclude>
</excludes>
</fileSet>
</fileSets>
</assembly>
82.5.2 工作流程
工作流程看起来类似于Step by step guide to CDC
中提供的工作流程。唯一的区别是 producer 不再拥有 contracts。所以 consumer 和 producer 必须在 common repository 中使用 common contracts。
82.5.3 消费者
当consumer希望在 contracts 离线时工作,而不是克隆 producer code,consumer 团队克隆 common repository,转到所需的 producer 的文件夹(e.g .com/example/server
)并运行mvn clean install -DskipTests
以在本地安装转换的存根来自 contracts。
你需要Maven 在本地安装
82.5.4 Producer
作为producer,它足以改变 Spring Cloud Contract Verifier 以提供包含 contracts 的 JAR 的 URL 和依赖关系:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
<contractDependency>
<groupId>com.example.standalone</groupId>
<artifactId>contracts</artifactId>
</contractDependency>
</configuration>
</plugin>
通过此设置,将从http://link/to/your/nexus/or/artifactory/or/sth
下载带有 groupid com.example.standalone
和 artifactid contracts
的 JAR。然后将其解压缩到本地临时文件夹中,下的 contracts 将被选为用于生成测试和存根的 contracts。由于这个惯例,producer 团队将知道在完成一些不兼容的更改时哪些 consumer 团队将被破坏。
流的 rest 看起来是一样的。
82.6 我可以为测试设置多个 base classes 吗?
是!查看 Gradle 或 Maven 插件的contracts 的 base classes 不同部分。
82.7 如何调试生成的测试 client 发送的 request/response?
生成的测试都以某种形式或方式归结为 RestAssured,它依赖于Apache HttpClient。 HttpClient 有一个名为wire logging的工具,它将整个请求和响应记录到 HttpClient。 Spring Boot 有一个 logging common application property用于执行此类操作,只需将其添加到 application properties 中
logging.level.org.apache.http.wire=DEBUG
82.7.1 如何调试 WireMock 发送的 mapping/request/response?
从 version 1.2.0
开始,我们将 WireMock logging 打开为 info,将 WireMock 通知程序设置为详细。现在,您将准确了解 WireMock 服务器收到的请求以及选择了哪个匹配的响应定义。
要关闭此 feature,只需将 WireMock logging 碰到ERROR
logging.level.com.github.tomakehurst.wiremock=ERROR
82.7.2 如何查看在 HTTP 服务器存根中注册的内容?
您可以使用@AutoConfigureStubRunner
或StubRunnerRule
上的mappingsOutputFolder
property 来转储每个 artifact id 的所有映射。此外,还将附加启动给定存根服务器的 port。
82.7.3 我可以从响应中引用请求吗?
是!有了 version 1.1.0,我们就增加了这种可能性。在 HTTP 存根服务器端,我们为 WireMock 提供支持。在其他 HTTP 服务器存根的情况下,您必须自己实现该方法。
82.7.4 我可以从文件中引用文字吗?
是!有了 version 1.2.0,我们就增加了这种可能性。在 DSL 中调用file(…)
方法并提供相对于 contract 放置位置的路径就足够了。如果您正在使用 YAML,请使用bodyFromFile
property。