88. Spring Cloud Contract 常见问题解答

88.1 为什么使用 Spring Cloud Contract Verifier 而不使用 X?

目前,Spring Cloud Contract 是基于 JVM 的工具。因此,当您已经为 JVM 创建软件时,它可能是您的首选。这个项目具有很多非常有趣的功能,但是特别是其中的许多功能无疑使 Spring Cloud Contract Verifier 在 Consumer 驱动 Contract(CDC)工具的“市场”上脱颖而出。最有趣的是:

  • 通过消息进行 CDC 的可能性

  • 清晰易用的静态类型 DSL

  • 可以将您当前的 JSON 文件复制粘贴到 Contract 中,并仅编辑其元素

  • 根据定义的 Contract 自动生成测试

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

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

  • Spring Cloud Contract 与 Pact 开箱即用地集成在一起,并提供了简单的钩子来扩展其功能

  • 通过 Docker 添加了对使用的任何语言和框架的支持

88.2 我不想用 Groovy 编写 Contract!

没问题。您可以在 YAML 中写 Contract!

88.3 这个值是什么(consumer(),producer())?

与存根相关的最大挑战之一是它们的可重用性。只有将它们广泛使用,它们才能达到目的。通常使困难的是请求/响应元素的硬编码值。例如日期或 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"
}

想象一下通过更改系统中的时钟或提供数据提供程序的存根实现来设置time字段的适当值(让我们假设此内容是由数据库生成)所需的痛苦。与名为id的字段相同。您将创建 UUID 生成器的存根实现吗?毫无意义...

因此,作为 Consumer,您希望发送与任何时间形式或任何 UUID 相匹配的请求。这样,您的系统将像往常一样工作-会生成数据,而您无需存根任何东西。假设在上述 JSON 中,最重要的部分是body字段。您可以专注于此并为其他字段提供匹配。换句话说,您希望存根像这样工作:

{
    "time" : "SOMETHING THAT MATCHES TIME",
    "id" : "SOMETHING THAT MATCHES UUID",
    "body" : "foo"
}

就响应作为 Consumer 而言,您需要可以操作的具体价值。因此,这样的 JSON 是有效的

{
    "time" : "2016-10-10 21:10:15",
    "id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
    "body" : "bar"
}

如您在前几节中所看到的,我们根据 Contract 生成测试。因此,从生产者的角度来看,情况似乎大不相同。我们正在解析提供的 Contract,并且在测试中我们想向您的端点发送真实请求。因此,对于请求的生产者而言,我们无法进行任何形式的匹配。我们需要生产者后端可以使用的具体价值。这样的 JSON 是有效的:

{
    "time" : "2016-10-10 20:10:15",
    "id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
    "body" : "foo"
}

另一方面,从 Contract 有效性的角度来看,响应不一定必须包含timeid的具体值。假设您是在生产者端生成的-再次,您必须进行大量的存根操作以确保始终返回相同的值。因此,从生产者的角度来看,您可能想要以下响应:

{
    "time" : "SOMETHING THAT MATCHES TIME",
    "id" : "SOMETHING THAT MATCHES UUID",
    "body" : "bar"
}

那么,您如何才能一次为 Consumer 提供匹配器,为生产者提供具体价值,反之亦然?在 Spring Cloud Contract 中,我们允许您提供 动态值 。这意味着通信的双方可能会有所不同。您可以传递值:

通过value方法

value(consumer(...), producer(...))
value(stub(...), test(...))
value(client(...), server(...))

或使用$()方法

$(consumer(...), producer(...))
$(stub(...), test(...))
$(client(...), server(...))

您可以在第 93 章,ContractDSL部分中阅读有关此内容的更多信息。

调用value()$()告诉 Spring Cloud Contract 您将传递动态值。在consumer()方法内部,传递应该在使用者方(在生成的存根中)使用的值。在producer()方法内部,传递应该在生产方(在生成的测试中)使用的值。

Tip

如果一侧传递了正则表达式,而另一侧则没有传递,则另一侧将自动生成。

通常,您会将该方法与regex helper 方法一起使用。例如。 consumer(regex('[0-9]{10}'))

综上所述,上述情况的 Contract 看起来或多或少像这样(时间和 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"
					])
			}
}

Tip

请阅读与 JSON 相关的 Groovy 文档,以了解如何正确构造请求/响应正文。

88.4 如何进行存根版本控制?

88.4.1 API 版本控制

让我们尝试回答一个问题,即版本控制的 true 含义。如果您指的是 API 版本,则有不同的方法。

  • 使用超媒体,链接,并且不以任何方式对您的 API 进行版本控制

  • 通过标题/网址传递版本

我不会尝试回答哪种方法更好的问题。应选择适合您需求并允许您产生业务价值的任何东西。

假设您对 API 进行了版本控制。在这种情况下,您应提供与所支持版本一样多的 Contract。您可以为每个版本创建一个子文件夹,也可以将其附加到 Contract 名称之后-更加适合您。

88.4.2 JAR 版本

如果用版本控制来表示包含存根的 JAR 版本,则实质上有两种Main 方法。

假设您正在执行持续交付/部署,这意味着您每次通过管道都将生成一个新版本的 jar,并且该 jar 可以随时投入生产。例如,您的 jar 版本如下所示(它构建于 20.10.2016 at 20:15:21):

1.0.0.20161020-201521-RELEASE

在这种情况下,您生成的存根Jar将如下所示。

1.0.0.20161020-201521-RELEASE-stubs.jar

在这种情况下,在引用存根时,您应该在application.yml@AutoConfigureStubRunner内部,以提供存根的最新版本。您可以通过传递+符号来实现。例

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

但是,如果版本是固定的(例如1.0.4.RELEASE2.1.1),则必须设置 jar 版本的具体值。 2.1.1 的示例。

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

88.4.3 开发或生产存根

您可以操纵分类器以针对其他服务或已部署到 Producing 的服务的存根的当前开发版本运行测试。如果您更改构建以在实现生产部署后使用prod-stubs分类器部署存根,则可以在一种情况下使用开发存根运行测试,在一种情况下使用产品存根运行测试。

使用存根开发版本的测试示例

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

使用生产版本的存根进行测试的示例

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

您还可以通过部署管道中的属性传递这些值。

88.5Contract 通用 repo

除了与生产者签订 Contract 之外,存储 Contract 的另一种方法是将 Contract 放在一个共同的地方。这可能与安全问题有关,在这些问题中,Consumer 无法克隆生产者的代码。同样,如果您将 Contract 放在一个地方,那么作为生产者,您将知道您有多少个 Consumer,以及将因本地变更而中断的 Consumer。

88.5.1repo 结构

假设我们有一个生产者,其坐标为com.example:server且 Consumer 为 3 个:client1client2client3。然后,在具有通用 Contract 的存储库中,您将具有以下设置(可以检出here):

├── com
│ └── example
│     └── server
│         ├── client1
│         │ └── expectation.groovy
│         ├── client2
│         │ └── expectation.groovy
│         ├── client3
│         │ └── expectation.groovy
│         └── pom.xml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    └── assembly
        └── contracts.xml

如您所见,在以斜杠分隔的 groupid /artifact id 文件夹(com/example/server)下,您对 3 个使用者(client1client2client3)有期望。期望是本文档中描述的标准 Groovy DSLContract 文件。该存储库必须生成一个 JAR 文件,该文件将仓库内容一一对应。

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.1.2.RELEASE</version>
		<relativePath />
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
		<spring-cloud-contract.version>2.1.1.BUILD-SNAPSHOT</spring-cloud-contract.version>
		<spring-cloud-release.version>Greenwich.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 插件之外,没有其他依赖项。Consumer 方必须运行这些 pom mvn clean install -DskipTests才能在本地安装生产者项目的存根。

根文件夹中的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>

它使用 Assembly 插件来构建包含所有 Contract 的 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>

88.5.2 Workflow

该工作流程看起来类似于Step by step guide to CDC中显示的工作流程。唯一的区别是,生产者不再拥有 Contract。因此,Consumer 和生产者必须在公用存储库中处理公用 Contract。

88.5.3 Consumer

Consumer 希望脱机处理 Contract 时,而不是克隆生产者代码,Consumer 团队将克隆公共存储库,转到所需的生产者文件夹(例如com/example/server),然后运行mvn clean install -DskipTests来将从中转换而来的存根安装到本地。Contract。

Tip

您需要拥有Maven 本地安装

88.5.4 Producer

作为“生产者”,只需更改 Spring Cloud Contract Verifier 即可提供 URL 和包含 Contract 的 JAR 依赖项:

<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下载具有组 ID com.example.standalone和工件 ID contracts的 JAR。然后将其解压缩到本地临时文件夹中,并选择com/example/server下的 Contract 作为生成测试和存根的 Contract。根据该约定,当完成一些不兼容的更改时,生产者团队将知道哪些 Consumer 团队将被破坏。

其余流程看起来相同。

88.5.5 如何按主题而不是按生产者定义消息传递 Contract?

为了避免通用仓库中的消息 Contract 重复,当很少有生产者将消息写到一个主题时,我们可以创建一个结构,将其余 Contract 放置在每个生产者的文件夹中,并将消息 Contract 放置在每个主题的文件夹中。

对于 Maven 项目

为了能够在生产者端进行工作,我们应该指定一个包含模式,以通过我们感兴趣的消息传递主题来过滤公共存储库 jar。Maven Spring Cloud Contract pluginincludedFiles属性允许我们执行此操作。另外,还需要指定contractsPath,因为默认路径是通用存储库groupid/artifactid

<plugin>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-contract-maven-plugin</artifactId>
   <version>${spring-cloud-contract.version}</version>
   <configuration>
      <contractsMode>REMOTE</contractsMode>
      <contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
      <contractDependency>
         <groupId>com.example</groupId>
         <artifactId>common-repo-with-contracts</artifactId>
         <version>+</version>
      </contractDependency>
      <contractsPath>/</contractsPath>
      <baseClassMappings>
         <baseClassMapping>
            <contractPackageRegex>.*messaging.*</contractPackageRegex>
            <baseClassFQN>com.example.services.MessagingBase</baseClassFQN>
         </baseClassMapping>
         <baseClassMapping>
            <contractPackageRegex>.*rest.*</contractPackageRegex>
            <baseClassFQN>com.example.services.TestBase</baseClassFQN>
         </baseClassMapping>
      </baseClassMappings>
      <includedFiles>
         <includedFile>**/${project.artifactId}/**</includedFile>
         <includedFile>**/${first-topic}/**</includedFile>
         <includedFile>**/${second-topic}/**</includedFile>
      </includedFiles>
   </configuration>
</plugin>

对于 Gradle 项目

  • 为 common-repo 依赖项添加定制配置:
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")
}
  • Unzip 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
}
  • 清理未使用的 Contract:
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属性指定包含 Contract 的目录来配置插件
contracts {
    contractsDslDir = new File("${buildDir}/unpackedContracts")
}

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

在多语言的世界中,有些语言不使用二进制存储,例如 Artifactory 或 Nexus。从 Spring Cloud Contract 版本 2.0.0 开始,我们提供了在 SCM 存储库中存储 Contract 和存根的机制。当前唯一支持的 SCM 是 Git。

存储库必须进行以下设置(您可以签出here):

.
└── 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(例如com.example)对应用程序进行分组

  • 然后每个应用程序都通过artifactId(例如beer-api-producer-git)表示

  • 接下来是应用程序的版本(例如0.0.1-SNAPSHOT)。从 Spring Cloud Contract 版本2.1.0开始,您可以指定以下版本(假设您的版本遵循语义版本)

  • +latest-查找存根的最新版本(假设快照始终是给定修订版本的最新工件)。这意味着:

  • 如果您使用的是1.0.0.RELEASE2.0.0.BUILD-SNAPSHOT2.0.0.RELEASE版本,我们将假定最新版本为2.0.0.BUILD-SNAPSHOT

    • 如果您使用的是1.0.0.RELEASE2.0.0.RELEASE版本,我们将假定最新版本为2.0.0.RELEASE

    • 如果您有一个名为latest+的版本,我们将选择该文件夹

    • release-查找存根的最新版本。这意味着:

  • 如果您使用的是1.0.0.RELEASE2.0.0.BUILD-SNAPSHOT2.0.0.RELEASE版本,我们将假定最新版本为2.0.0.RELEASE

    • 如果您有一个名为release的版本,我们将选择该文件夹
  • 最后,有两个文件夹:

  • contracts-最佳做法是将每个 Consumer 所需的 Contract 存储在具有 Consumer 名称(例如beer-api-consumer)的文件夹中。这样,您就可以使用stubs-per-consumer功能。进一步的目录结构是任意的。

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

88.6.1 协议约定

为了控制 Contract 来源的类型和位置(无论是二进制存储还是 SCM 存储库),可以在存储库 URL 中使用协议。 Spring Cloud Contract 遍历已注册的协议解析器,并尝试获取 Contract(通过插件)或存根(通过 Stub Runner)。

目前,对于 SCM 功能,我们支持 Git 存储库。要使用它,请在需要放置存储库 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

88.6.2 Producer

对于生产者,要使用 SCM 方法,我们可以重用与外部 Contract 相同的机制。我们通过包含git://协议的 URL 路由 Spring Cloud Contract 以使用 SCM 实现。

Tip

您必须在 Maven 中手动添加pushStubsToScm目标或在 Gradle 中执行(绑定)pushStubsToScm任务。我们不会开箱即用将存根推送到您 git 存储库的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>

Gradle.

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文件夹以查找 Contract。例如。对于com.example:foo:1.0.0,路径为META-INF/com.example/foo/1.0.0/contracts

  • 将根据 Contract 生成测试

  • 将根据 Contract 创建存根

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

  • 最后,将对该存储库的origin进行推送

88.6.3 生产者,Contract 存储在本地

使用 SCM 作为存根和 Contract 目的地的另一种选择是与生产者一起在本地存储 Contract,并且仅将 Contract 和存根推送到 SCM。在下面,您可以找到使用 Maven 和 Gradle 实现此功能所需的设置。

Maven.

<plugin>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
	<version>${spring-cloud-contract.version}</version>
	<extensions>true</extensions>
	<!-- In the default configuration, we want to use the contracts stored locally -->
	<configuration>
		<baseClassMappings>
			<baseClassMapping>
				<contractPackageRegex>.*messaging.*</contractPackageRegex>
				<baseClassFQN>com.example.BeerMessagingBase</baseClassFQN>
			</baseClassMapping>
			<baseClassMapping>
				<contractPackageRegex>.*rest.*</contractPackageRegex>
				<baseClassFQN>com.example.BeerRestBase</baseClassFQN>
			</baseClassMapping>
		</baseClassMappings>
		<basePackageForTests>com.example</basePackageForTests>
	</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>
			<configuration>
				<!-- We want to pick contracts from a Git repository -->
				<contractsRepositoryUrl>git://file://${env.ROOT}/target/contract_empty_git/</contractsRepositoryUrl>
				<!-- Example of URL via git protocol -->
				<!--<contractsRepositoryUrl>git://[emailprotected]:spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>-->
				<!-- Example of URL via http protocol -->
				<!--<contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-samples.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 mode can't be classpath -->
				<contractsMode>LOCAL</contractsMode>
			</configuration>
		</execution>
	</executions>
</plugin>

Gradle.

contracts {
		// Base package for generated tests
	basePackageForTests = "com.example"
	baseClassMappings {
		baseClassMapping(".*messaging.*", "com.example.BeerMessagingBase")
		baseClassMapping(".*rest.*", "com.example.BeerRestBase")
	}
}

/*
In this scenario we want to publish stubs to SCM whenever
the `publish` task is executed
*/
publishStubsToScm {
	// We want to modify the default set up of the plugin when publish stubs to scm is called
	customize {
		// 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://file://${System.getenv("ROOT")}/target/contract_empty_git/"
		}
		// The mode can't be classpath
		contractsMode = "LOCAL"
	}
}

publish.dependsOn("publishStubsToScm")
publishToMavenLocal.dependsOn("publishStubsToScm")

通过这样的设置:

  • 从默认的src/test/resources/contracts目录中选择 Contract

  • 将根据 Contract 生成测试

  • 将根据 Contract 创建存根

  • 一旦测试通过

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

    • 存根和 Contract 将在克隆的存储库中提交
  • 最后,将对该存储库的origin进行推送

与生产者和存根之间的 Contract 保持一致

也可以将 Contract 保留在生产者存储库中,但将存根保留在外部 git repo 中。当您想使用基本的 Consumer-生产者协作流程,但又无法使用工件存储库来存储存根时,这是最有用的。

为此,请使用通常的生产者设置,然后将pushStubsToScm目标添加并设置contractsRepositoryUrl到要保留存根的存储库中。

88.6.4 Consumer

在使用者方面,通过@AutoConfigureStubRunner注解,JUnit 规则,JUnit 5 扩展或属性传递repositoryRoot参数时,足以传递带有协议前缀的 SCM 存储库的 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/文件夹以查找存根定义和 Contract。例如。对于com.example:foo:1.0.0,路径为META-INF/com.example/foo/1.0.0/

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

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

88.7 我可以使用契约代理吗?

使用Pact时,您可以使用Pact Broker来存储和共享 Pact 定义。从 Spring Cloud Contract 2.0.0 开始,您可以从 Pact Broker 获取 Pact 文件以生成测试和存根。

作为前提条件,需要使用 Pact Converter 和 Pact Stub Downloader。您必须通过spring-cloud-contract-pact依赖项添加它们。您可以在第 95.1.1 节“契约转换器”部分中了解更多信息。

Tip

该条约遵循《ConsumerContract》约定。这意味着 Consumer 首先创建契约约定,然后与生产者共享文件。这些期望是由 Consumer 的代码产生的,如果不满足期望,则可能破坏生产者。

88.7.1 契约使用者

使用者使用 Pact 框架生成 Pact 文件。该契约文件将发送到契约代理。 here提供了此类设置的示例。

88.7.2 Producer

对于生产者,要使用 Pact Broker 中的 Pact 文件,我们可以重复使用与外部 Contract 相同的机制。我们通过包含pact://协议的 URL 路由 Spring Cloud Contract 以使用 Pact 实现。只需将 URL 传递给 Pact Broker。 here提供了此类设置的示例。

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>

Gradle.

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

通过这样的设置:

  • 契约文件将从契约代理下载

  • Spring Cloud Contract 将 Pact 文件转换为测试和存根

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

88.7.3 契约使用者(生产者 Contract 法)

在您不想执行“ConsumerContract”方法(为每个 Consumer 定义期望)但您更愿意执行“生产者 Contract”(生产者提供 Contract 并发布存根)的情况下,使用 Spring Cloud Contract 就足够了与 Stub Runner 选项一起使用。 here提供了此类设置的示例。

首先,请记住添加 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>

Gradle.

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 传递给带有pact://协议前缀的repositoryRoot即可。例如。 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;
    //...
}

通过这样的设置:

  • 契约文件将从契约代理下载

  • Spring Cloud Contract 将 Pact 文件转换为存根定义

  • 存根服务器将启动并被存入存根

有关 Pact 支持的更多信息,请转到第 95.7 节“使用契约存根下载器”部分。

88.8 如何调试由生成的测试 Client 端发送的请求/响应?

生成的测试全部以依赖于Apache HttpClient的某种形式或方式归结为 RestAssured。 HttpClient 具有一个名为wire logging的工具,该工具会将整个请求和响应记录到 HttpClient 中。 Spring Boot 有一个日志记录通用应用程序属性可以执行这种操作,只需将其添加到您的应用程序属性中

logging.level.org.apache.http.wire=DEBUG

88.8.1 如何调试 WireMock 发送的 Map/请求/响应?

从版本1.2.0开始,我们将 WireMock 日志记录打开到 info,而将 WireMock 通知程序打开为冗长。现在,您将完全知道 WireMock 服务器收到了什么请求,以及选择了哪个匹配的响应定义。

要关闭此功能,只需将 WireMock 日志记录更改为ERROR

logging.level.com.github.tomakehurst.wiremock=ERROR

88.8.2 如何查看 HTTP 服务器存根中注册了什么?

您可以使用@AutoConfigureStubRunnerStubRunnerRule或``上的mappingsOutputFolder属性来按工件 ID 转储所有 Map。另外,将连接启动给定存根服务器的端口。

88.8.3 我可以引用文件中的文本吗?

是!在 1.2.0 版中,我们添加了这种可能性。在 DSL 中调用file(…)方法并提供相对于 Contract 所在位置的路径就足够了。如果您使用的是 YAML,则只需使用bodyFromFile属性。