90. Spring Cloud Contract 常见问题解答

90.1 为什么要使用 Spring Cloud Contract Verifier 而不是 X?

对于 time 是 Spring Cloud Contract 是一个基于 JVM 的工具。因此,当您已经为 JVM 创建软件时,它可能是您的首选。这个项目有很多非常有趣的 features,但特别是其中一些肯定会让 Spring Cloud Contract Verifier 在 Consumer Driven Contract(CDC)工具的“市场”中脱颖而出。在许多最有趣的是:

  • 可以通过消息传递进行 CDC

  • 清晰易用,静态类型的 DSL

  • 可以将当前 JSON 文件粘贴到 contract 并仅编辑其元素

  • 从定义的 Contract 自动生成测试

  • Stub Runner 功能 - 存根在运行时从 Nexus/Artifactory 自动下载

  • Spring Cloud integration - integration 测试不需要发现服务

  • Spring Cloud Contract 与 Pact 开箱即用,并提供简单的钩子来扩展其功能

  • Via Docker 增加了对所用语言和 framework 的支持

90.2 我不想在 Groovy 中写一个 contract!

没问题。你可以在 YAML 写一个 contract!

90.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 的有效性的角度来看,响应不一定必须包含timeid的具体值。假设您在 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(...))

您可以在第 95 章,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 OK()
				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以了解如何正确构建请求/响应主体。

90.4 如何进行 Stubs 版本控制?

90.4.1 API 版本控制

让我们试着回答一个问题版本真正意味着什么。如果您指的是 API version,那么有不同的方法。

  • 使用超媒体,链接,不要以任何方式 version 你的 API

  • 通过 headers/urls 传递版本

我不会试图回答哪个方法更好的问题。无论什么适合您的需求,并允许您生成业务 value 应该被选中。

我们假设你做了 version 你的 API。在这种情况下,您应该提供与您支持的许多版本一样多的 contracts。您可以为每个 version 创建一个子文件夹,或者将其附加到 contract name - 任何适合您的内容。

90.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.RELEASE2.1.1),那么你必须设置 jar version 的具体 value。 例如 2.1.1.

@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:2.1.1:stubs:8080"})

90.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 传递这些值。

90.5 Common repo with contracts

存储 contracts 而不是将它们与 producer 一起存储的另一种方法是将它们保存在 common 位置。它可能与消费者无法克隆 producer 的 code 的安全问题有关。此外,如果您将 contracts 保留在一个地方,那么作为一个 producer,您将知道您拥有多少消费者以及哪些消费者将根据您的本地更改进行 break。

90.5.1 Repo 结构

假设我们有一个带有坐标com.example:server和 3 个消费者的 producer:client1client2client3。然后在包含 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 个消费者(client1client2client3)的期望。期望是本文档中描述的标准 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>2.0.6.RELEASE</version>
		<relativePath />
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
		<spring-cloud-contract.version>2.0.3.BUILD-SNAPSHOT</spring-cloud-contract.version>
		<spring-cloud-release.version>Finchley.BUILD-SNAPSHOT</spring-cloud-release.version>
		<excludeBuildFolders>true</excludeBuildFolders>
	</properties>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud-release.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>

90.5.2 工作流程

工作流程看起来类似于Step by step guide to CDC中提供的工作流程。唯一的区别是 producer 不再拥有 contracts。所以 consumer 和 producer 必须在 common repository 中使用 common contracts。

90.5.3 消费者

consumer希望在 contracts 离线时工作,而不是克隆 producer code,consumer 团队克隆 common repository,转到所需的 producer 的文件夹(e.g .com/example/server)并运行mvn clean install -DskipTests以在本地安装转换的存根来自 contracts。

你需要Maven 在本地安装

90.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>
		<contractsMode>REMOTE</contractsMode>
		<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 看起来是一样的。

90.5.5 如何根据 producer 定义每个 topic 的消息 contracts?

为了避免消息传递 contracts 在 common repo 中重复,当少数生成器将消息写入一个 topic 时,我们可以创建结构,当 rest contracts 被放置在每个 producer 的文件夹中,并且每个 topic 的文件夹中都有 contracts。

对于 Maven 项目

为了能够在 producer 端工作,我们可以做以下事情(所有这些都通过 Maven 插件):

  • 将 common repo 依赖项添加到 classpath:
<dependency>
   <groupId>com.example</groupId>
   <artifactId>common-repo</artifactId>
   <version>${common-repo.version}</version>
</dependency>
  • 使用 contracts 下载 JAR 并将 JAR 解压缩到目标:
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-dependency-plugin</artifactId>
   <version>3.0.0</version>
   <executions>
      <execution>
         <id>unpack-dependencies</id>
         <phase>process-resources</phase>
         <goals>
            <goal>unpack</goal>
         </goals>
         <configuration>
            <artifactItems>
               <artifactItem>
                  <groupId>com.example</groupId>
                  <artifactId>common-repo</artifactId>
                  <type>jar</type>
                  <overWrite>false</overWrite>
                  <outputDirectory>${project.build.directory}/contracts</outputDirectory>
               </artifactItem>
            </artifactItems>
         </configuration>
      </execution>
   </executions>
</plugin>
  • 撕掉我们不感兴趣的所有文件夹:
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-antrun-plugin</artifactId>
   <version>1.8</version>
   <executions>
      <execution>
         <phase>process-resources</phase>
         <goals>
            <goal>run</goal>
         </goals>
         <configuration>
            <tasks>
               <delete includeemptydirs="true">
                  <fileset dir="${project.build.directory}/contracts">
                     <include name="**/*" />
                     <!--Producer artifactId-->
                     <exclude name="**/${project.artifactId}/**" />
                     <!--List of the supported topics-->
                     <exclude name="**/${first-topic}/**" />
                     <exclude name="**/${second-topic}/**" />
                  </fileset>
               </delete>
            </tasks>
         </configuration>
      </execution>
   </executions>
</plugin>
  • 通过指向 contracts 到目标下的文件夹来运行 contract 插件:
<plugin>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-contract-maven-plugin</artifactId>
   <version>${spring-cloud-contract.version}</version>
   <extensions>true</extensions>
   <configuration>
      <packageWithBaseClasses>com.example</packageWithBaseClasses>
      <baseClassMappings>
         <baseClassMapping>
            <contractPackageRegex>.*intoxication.*</contractPackageRegex>
            <baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN>
         </baseClassMapping>
      </baseClassMappings>
      <contractsDirectory>${project.build.directory}/contracts</contractsDirectory>
   </configuration>
</plugin>

适用于 Gradle Project

  • 为 common-repo 依赖项添加自定义 configuration:
ext {
    conractsGroupId = "com.example"
    contractsArtifactId = "common-repo"
    contractsVersion = "1.2.3"
}

configurations {
    contracts {
        transitive = false
    }
}
  • 将 common-repo 依赖项添加到 classpath:
dependencies {
    contracts "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    testCompile "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
}
  • 将依赖项下载到适当的文件夹:
task getContracts(type: Copy) {
    from configurations.contracts
    into new File(project.buildDir, "downloadedContracts")
}
  • 解压缩 JAR:
task unzipContracts(type: Copy) {
    def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar")
    def outputDir = file("${buildDir}/unpackedContracts")

    from zipTree(zipFile)
    into outputDir
}
  • 清理未使用的 contracts:
task deleteUnwantedContracts(type: Delete) {
    delete fileTree(dir: "${buildDir}/unpackedContracts",
        include: "**/*",
        excludes: [
            "**/${project.name}/**"",
            "**/${first-topic}/**",
            "**/${second-topic}/**"])
}
  • 创建任务依赖项:
unzipContracts.dependsOn("getContracts")
deleteUnwantedContracts.dependsOn("unzipContracts")
build.dependsOn("deleteUnwantedContracts")
  • 通过使用contractsDslDir property 指定包含 contracts 的目录来配置插件
contracts {
    contractsDslDir = new File("${buildDir}/unpackedContracts")
}

90.6 我需要二进制存储吗?我不能用 Git 吗?

在多语言世界中,有些语言不使用像 Artifactory 或 Nexus 这样的二进制存储。从 Spring Cloud Contract version 2.0.0 开始,我们提供了在 SCM repository 中 store contracts 和 stubs 的机制。目前唯一支持的 SCM 是 Git。

repository 必须进行以下设置(你可以签出这里):

.
└── META-INF
    └── com.example
        └── beer-api-producer-git
            └── 0.0.1-SNAPSHOT
                ├── contracts
                │ └── beer-api-consumer
                │     ├── messaging
                │     │ ├── shouldSendAcceptedVerification.groovy
                │     │ └── shouldSendRejectedVerification.groovy
                │     └── rest
                │         ├── shouldGrantABeerIfOldEnough.groovy
                │         └── shouldRejectABeerIfTooYoung.groovy
                └── mappings
                    └── beer-api-consumer
                        └── rest
                            ├── shouldGrantABeerIfOldEnough.json
                            └── shouldRejectABeerIfTooYoung.json

META-INF文件夹下:

  • 我们通过groupId(e.g. com.example)group applications

  • 然后每个 application 都通过artifactId(e.g. beer-api-producer-git)表示

  • 接下来,application 的 version。 version 是强制性的! (e.g. 0.0.1-SNAPSHOT)

  • 最后,有两个文件夹:

  • contracts - 最好的做法是使用 consumer name(e.g. beer-api-consumer)存储文件夹中每个 consumer 所需的 contracts。这样你就可以使用stubs-per-consumer feature。更多目录结构是任意的。

  • mappings - 在此文件夹中 Maven/Gradle Spring Cloud Contract 插件将推送存根服务器映射。在 consumer 端,Stub Runner 将扫描此文件夹以使用存根定义启动存根服务器。文件夹结构将是contracts子文件夹中创建的文件夹的副本。

90.6.1 议定书公约

在 order 中控制 contracts 源的类型和位置(无论是二进制存储还是 SCM repository),您可以在 repository 的 URL 中使用该协议。 Spring Cloud Contract 迭代注册协议解析器并尝试获取 contracts(通过插件)或存根(通过 Stub Runner)。

对于 SCM 功能,目前我们支持 Git repository。要使用它,在 property 中需要放置 repository URL,您只需要在连接 URL 前加上git://。在这里你可以找到几个例子:

git://file:///foo/bar
git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git
git://[emailprotected]:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git

90.6.2 Producer

对于 producer,要使用 SCM 方法,我们可以重用我们用于外部 contracts 的相同机制。我们 route Spring Cloud Contract 通过包含git://协议的 URL 使用 SCM implementation。

您必须在 Maven 中手动添加pushStubsToScm目标或在 Gradle 中执行(绑定)pushStubsToScm任务。我们不会将存根推送到 git repository 的origin开箱即用。

Maven 的.

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <!-- Base class mappings etc. -->

        <!-- We want to pick contracts from a Git repository -->
        <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl>

        <!-- We reuse the contract dependency section to set up the path
        to the folder that contains the contract definitions. In our case the
        path will be /groupId/artifactId/version/contracts -->
        <contractDependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>${project.artifactId}</artifactId>
            <version>${project.version}</version>
        </contractDependency>

        <!-- The contracts mode can't be classpath -->
        <contractsMode>REMOTE</contractsMode>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <!-- By default we will not push the stubs back to SCM,
                you have to explicitly add it as a goal -->
                <goal>pushStubsToScm</goal>
            </goals>
        </execution>
    </executions>
</plugin>

摇篮.

contracts {
	// We want to pick contracts from a Git repository
	contractDependency {
		stringNotation = "${project.group}:${project.name}:${project.version}"
	}
	/*
	We reuse the contract dependency section to set up the path
	to the folder that contains the contract definitions. In our case the
	path will be /groupId/artifactId/version/contracts
	 */
	contractRepository {
		repositoryUrl = "git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git"
	}
	// The mode can't be classpath
	contractsMode = "REMOTE"
	// Base class mappings etc.
}

/*
In this scenario we want to publish stubs to SCM whenever
the `publish` task is executed
*/
publish.dependsOn("publishStubsToScm")

有了这样的设置:

  • Git 项目将被克隆到一个临时目录

  • SCM 存根下载器将转到META-INF/groupId/artifactId/version/contracts文件夹以查找 contracts。 E.g。对于com.example:foo:1.0.0,路径将是META-INF/com.example/foo/1.0.0/contracts

  • 测试将从 contracts 生成

  • 将从 contracts 创建存根

  • 测试通过后,存根将在克隆的 repository 中提交

  • 最后,将推动 repo 的origin

保持 contracts 与外部 repository 中的 producer 和 stubs

也可以将 contracts 保留在 producer repository 中,但将存根保留在外部 git repo 中。当您想要使用基本 consumer-producer 协作流时,这是最有用的,但是没有可能使用 artifact repository 来存储存根。

为了做到这一点,使用通常的 producer 设置,然后添加pushStubsToScm目标并将contractsRepositoryUrl设置到要保留存根的 repository。

90.6.3 消费者

在 consumer 端传递repositoryRoot参数时,无论是来自@AutoConfigureStubRunner annotation,JUnit 规则还是 properties,它都足以传递 SCM repository 的 URL,前缀为协议。例如

@AutoConfigureStubRunner(
    stubsMode="REMOTE",
    repositoryRoot="git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git",
    ids="com.example:bookstore:0.0.1.RELEASE"
)

有了这样的设置:

  • Git 项目将被克隆到一个临时目录

  • SCM 存根下载器将转到META-INF/groupId/artifactId/version/文件夹以查找存根定义和 contracts。 E.g。对于com.example:foo:1.0.0,路径将是META-INF/com.example/foo/1.0.0/

  • Stub 服务器将启动并提供映射

  • 将在消息传递测试中读取和使用消息传递定义

90.7 我可以使用 Pact Broker 吗?

使用协议时,您可以使用契约 Broker来 store 并共享 Pact 定义。从 Spring Cloud Contract 2.0.0 开始,可以从 Pact Broker 中获取 Pact files 来生成测试和存根。

作为先决条件,需要 Pact Converter 和 Pact Stub Downloader。您必须通过spring-cloud-contract-pact依赖项添加它。您可以在第 97.1.1 节,“契约转换器”部分阅读更多相关信息。

契约遵循 Consumer Contract 约定。这意味着 Consumer 首先创建 Pact 定义,然后与 Producer 共享 files。这些期望是从 Consumer 的 code 生成的,如果没有达到预期,可以 break_Producer。

90.7.1 Pact Consumer

consumer 使用 Pact framework 生成 Pact files。 Pact files 被发送到 Pact Broker。可以找到这里的这种设置的示例。

90.7.2 Producer

对于 producer,要使用 Pact Broker 中的 Pact files,我们可以重用我们用于外部 contracts 的相同机制。我们 route Spring Cloud Contract 通过包含pact://协议的 URL 使用 Pact implementation。将 URL 传递给 Pact Broker 就足够了。可以找到这里的这种设置的示例。

Maven 的.

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <!-- Base class mappings etc. -->

        <!-- We want to pick contracts from a Git repository -->
        <contractsRepositoryUrl>pact://http://localhost:8085</contractsRepositoryUrl>

        <!-- We reuse the contract dependency section to set up the path
        to the folder that contains the contract definitions. In our case the
        path will be /groupId/artifactId/version/contracts -->
        <contractDependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>${project.artifactId}</artifactId>
            <!-- When + is passed, a latest tag will be applied when fetching pacts -->
            <version>+</version>
        </contractDependency>

        <!-- The contracts mode can't be classpath -->
        <contractsMode>REMOTE</contractsMode>
    </configuration>
    <!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-pact</artifactId>
            <version>${spring-cloud-contract.version}</version>
        </dependency>
    </dependencies>
</plugin>

摇篮.

buildscript {
	repositories {
		//...
	}

	dependencies {
		// ...
		// Don't forget to add spring-cloud-contract-pact to the classpath!
		classpath "org.springframework.cloud:spring-cloud-contract-pact:${contractVersion}"
	}
}

contracts {
	// When + is passed, a latest tag will be applied when fetching pacts
	contractDependency {
		stringNotation = "${project.group}:${project.name}:+"
	}
	contractRepository {
		repositoryUrl = "pact://http://localhost:8085"
	}
	// The mode can't be classpath
	contractsMode = "REMOTE"
	// Base class mappings etc.
}

有了这样的设置:

  • Pact files 将从 Pact Broker 下载

  • Spring Cloud Contract 会将 Pact files 转换为测试和存根

  • 带有存根的 JAR 会像往常一样自动创建

90.7.3 Pact Consumer(Producer Contract approach)

在你不想做 Consumer Contract 方法的场景中(对于每个 consumer 定义期望)但是你更喜欢 Producer Contracts(producer 提供 contracts 和发布存根),它足以使用 Spring Cloud Contract 使用 Stub Runner 选项。可以找到这里的这种设置的示例。

首先,请记住将 Stub Runner 和 Spring Cloud Contract Pact 模块添加为测试依赖项。

Maven 的.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-pact</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

摇篮.

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {
    //...
    testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
    // Don't forget to add spring-cloud-contract-pact to the classpath!
    testCompile("org.springframework.cloud:spring-cloud-contract-pact")
}

接下来,只需将 Pact Broker 的 URL 传递给repositoryRoot,前缀为pact:// protocol。 E.g。 pact://http://localhost:8085

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.REMOTE,
		ids = "com.example:beer-api-producer-pact",
		repositoryRoot = "pact://http://localhost:8085")
public class BeerControllerTest {
    //Inject the port of the running stub
    @StubRunnerPort("beer-api-producer-pact") int producerPort;
    //...
}

有了这样的设置:

  • Pact files 将从 Pact Broker 下载

  • Spring Cloud Contract 会将 Pact files 转换为存根定义

  • 将启动存根服务器并使用存根提供

有关 Pact 支持的更多信息,您可以转到第 97.7 节,“使用 Pact Stub Downloader”部分。

90.8 如何调试生成的测试 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

90.8.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

90.8.2 如何查看在 HTTP 服务器存根中注册的内容?

您可以使用@AutoConfigureStubRunnerStubRunnerRule上的mappingsOutputFolder property 来转储每个 artifact id 的所有映射。此外,还将附加启动给定存根服务器的 port。

90.8.3 我可以从文件中引用文字吗?

是!有了 version 1.2.0,我们就增加了这种可能性。在 DSL 中调用file(…)方法并提供相对于 contract 放置位置的路径就足够了。如果您正在使用 YAML,请使用bodyFromFile property。