“操作指南”指南

1. 为什么使用 Spring Cloud Contract?

Spring Cloud Contract 在多语言环境中表现优异。该项目拥有许多非常有趣的功能。其中不少功能无疑使 Spring Cloud Contract Verifier 在消费者驱动契约(CDC)工具市场中脱颖而出。最引人注目的功能包括以下几点:spring-doc.cadn.net.cn

使用其他语言编写契约的方法

你可以使用YAML编写契约。有关更多信息,请参见此部分spring-doc.cadn.net.cn

我们正在努力提供更多描述契约的方式。您可以通过 github-issues 查看更多信息。spring-doc.cadn.net.cn

3. 我如何为合同提供动态值?

关于桩(stubs)最大的挑战之一是它们的可重用性。只有当它们能够被广泛使用时,才能实现其目的。请求和响应元素中的硬编码值(如日期和ID)通常会使其变得困难。请考虑以下JSON请求:spring-doc.cadn.net.cn

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

现在考虑以下 JSON 响应:spring-doc.cadn.net.cn

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

想象一下,通过更改系统时钟或提供数据提供程序的存档实现来正确设置time字段(假设此内容由数据库生成)所需的痛苦。 字段称为id也是如此。 您可以创建UUID生成器的存档实现,但这样做几乎没有意义。spring-doc.cadn.net.cn

因此,作为消费者,您希望发送一个与任何时间格式或任何 UUID 匹配的请求。这样,您的系统可以像往常一样工作,生成数据而无需进行任何模拟(stub)操作。假设在上述 JSON 中,最关键的部分是 body 字段。您可以专注于该字段,并为其他字段提供匹配规则。换句话说,您希望模拟服务按如下方式工作:spring-doc.cadn.net.cn

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

就响应而言,作为消费者,您需要一个具体的值,以便对其进行操作。因此,以下 JSON 是有效的:spring-doc.cadn.net.cn

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

在前面几节中,我们从契约生成了测试。因此,从生产者一方来看,情况就大不相同了。我们解析提供的契约,并且在测试中,我们希望向您的端点发送真实的请求。因此,对于生产者发起的请求情形,我们无法进行任何形式的匹配。我们需要具体的值,以便生产者的后端能够正常工作。因此,以下 JSON 将是有效的:spring-doc.cadn.net.cn

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

另一方面,从合同有效性的角度来看,响应并不一定必须包含值 0 或 1 的具体值。假设你在生成者端生成这些。同样,您必须进行大量的归档,以确保始终返回相同的值。这就是为什么,在生成器侧,您可能希望以下响应:spring-doc.cadn.net.cn

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

那么,您如何为消费者提供一个匹配器,同时为生产者提供一个具体值(反之亦然,在其他时间)?<br/>Spring Cloud Contract 允许您提供动态值。这意味着它在通信双方都可以不同。spring-doc.cadn.net.cn

您可以在 合同 DSL 部分了解更多信息。spring-doc.cadn.net.cn

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

4. 如何进行存根版本控制?

此节介绍了存根版本,您可以以多种方式处理它们:spring-doc.cadn.net.cn

4.1. API 版本控制

版本控制到底意味着什么?如果你指的是 API 版本,那么有多种不同的实现方式:spring-doc.cadn.net.cn

我们并不试图回答哪种方法更好的问题。您应选择适合您需求并能为您创造商业价值的方法。spring-doc.cadn.net.cn

假设您对API进行版本控制。在这种情况下,您应为所支持的每个版本提供尽可能多的契约(contract)。您可以为每个版本创建一个子文件夹,或将其附加到契约名称上——无论哪种方式最适合您。spring-doc.cadn.net.cn

4.2. JAR 版本控制

如果您的意思是JAR文件中包含存根的版本,则主要有两种方法。spring-doc.cadn.net.cn

假设您正在进行持续交付和部署,这意味着每次通过流水线时都会生成一个新的 JAR 文件,并且该 JAR 文件可随时部署到生产环境。例如,您的 JAR 文件版本可能如下所示(因为其构建于 2016 年 10 月 20 日 20:15:21):spring-doc.cadn.net.cn

1.0.0.20161020-201521-RELEASE

在这种情况下,生成的存根 JAR 文件应如下所示:spring-doc.cadn.net.cn

1.0.0.20161020-201521-RELEASE-stubs.jar

在这种情况下,您应在引用存根时,在application.yml@AutoConfigureStubRunner内部提供最新版本的存根。您可以通过传递+符号来实现这一点。以下示例展示了如何操作:spring-doc.cadn.net.cn

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

然而,如果版本控制是固定的(例如,1.0.4.RELEASE2.1.1),则必须设置 JAR 版本的具体值。以下示例展示了如何为版本 2.1.1 执行此操作:spring-doc.cadn.net.cn

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

{ {content} }

您可以操纵分类器,在开发版本中运行测试,或对部署到生产环境的其他服务存档进行测试。如果您将构建配置为在达到生产部署时与prod-stubs分类器一起部署存档,您就可以在同一用例中一次使用开发存档,一次使用生产存档。spring-doc.cadn.net.cn

以下示例适用于使用存根开发版本进行的测试:spring-doc.cadn.net.cn

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

以下示例适用于使用存根生产版本进行测试的情况:spring-doc.cadn.net.cn

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

您还可以在部署流水线中通过属性传递这些值。spring-doc.cadn.net.cn

5. 如何使用带有契约的公共 Repository 而不是由生产者存储它们?

另一种存储合约的方式,而不是让它们与生产者在一起,而是将其保存在一个公共位置。这种情况下可能与安全问题有关(即消费者无法克隆生产者的代码)。另外,如果您将合约集中保存在一个地方,那么作为生产者,您就知道有多少个消费者以及哪些消费者可能会因您的本地更改而中断。spring-doc.cadn.net.cn

5.1. 仓库结构

假设我们有一个生产者,其坐标为com.example:server,以及三个消费者:client1client2client3。 然后,在具有通用合约的存储库中,您可以拥有以下设置(您可以在这里检查)。 下面的清单显示了这样的结构:spring-doc.cadn.net.cn

├── 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),您有三个使用者的期望值(client1client2client3)。期望值是标准的Groovy DSL合同文件,如本文档所述。此存储库必须产生一个JAR文件,该文件一对一映射到存储库的内容。spring-doc.cadn.net.cn

以下示例显示了pom.xml位于server文件夹内的内容:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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</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.3.10.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <spring-cloud-contract.version>2.2.8.BUILD-SNAPSHOT</spring-cloud-contract.version>
        <spring-cloud-release.version>Hoxton.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插件外,没有其他依赖项。这些pom文件对于消费者端运行mvn clean install -DskipTests以本地安装生产者项目的存根是必要的。spring-doc.cadn.net.cn

根文件夹中的pom.xml可能如下所示:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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</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>

它使用装配插件构建包含所有契约的 JAR 文件。以下示例展示了此类设置:spring-doc.cadn.net.cn

<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://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>

5.2. 工作流

该工作流程假设在消费者和生产者端都设置了 Spring Cloud Contract。在包含契约的公共仓库中还正确配置了插件。CI作业设置为构建所有契约并将其上传到 Nexus/Artifactory 的公共仓库。</br></br>下图显示了此工作流程的 UML:spring-doc.cadn.net.cn

how to common repo

5.3. 消费者

当消费者希望离线处理契约时,与其克隆生产者代码,不如由消费者团队克隆公共仓库,进入所需生产者文件夹(例如 com/example/server),然后运行 mvn clean install -DskipTests 以本地安装由契约转换而来的存根。spring-doc.cadn.net.cn

你需要安装本地Maven

5.4. 生产者

作为生产者,您可以更改 Spring Cloud Contract Verifier 以提供包含契约的 JAR 的 URL 和依赖项,如下所示:spring-doc.cadn.net.cn

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <configuration>
        <contractsMode>REMOTE</contractsMode>
        <contractsRepositoryUrl>
            https://link/to/your/nexus/or/artifactory/or/sth
        </contractsRepositoryUrl>
        <contractDependency>
            <groupId>com.example.standalone</groupId>
            <artifactId>contracts</artifactId>
        </contractDependency>
    </configuration>
</plugin>

通过此设置,从link/to/your/nexus/or/artifactory/or/sth下载groupid为com.example.standalone和artifactidcontracts的JAR。然后将其解压缩到本地临时文件夹中,并选择com/example/server中存在的合同作为生成测试和存根所使用的合同。由于这个约定,生产者团队可以知道在进行一些不兼容更改时哪些消费者团队会受到影响。spring-doc.cadn.net.cn

流程的其余部分看起来相同。spring-doc.cadn.net.cn

Spring for Apache Kafka 5.5.如何按主题而不是按生产者定义消息契约?

为避免在通用仓库中重复消息契约,当多个生产者向同一主题写入消息时,我们可以创建一种结构:将 REST 契约放置在每个生产者对应的文件夹中,而消息契约则放置在每个主题对应的文件夹中。spring-doc.cadn.net.cn

5.5.1. 基于Maven的项目

为了能够在生产者端进行工作,我们需要指定一个包含模式,以根据我们感兴趣的消息主题来过滤常见的存储库 JAR 文件。Maven Spring Cloud Contract 插件的 includedFiles 属性可实现此目的。此外,contractsPath 也必须被指定,因为默认路径将是常见的存储库 groupid/artifactid。以下示例展示了一个用于 Spring Cloud Contract 的 Maven 插件:spring-doc.cadn.net.cn

<plugin>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-contract-maven-plugin</artifactId>
   <version>${spring-cloud-contract.version}</version>
   <configuration>
      <contractsMode>REMOTE</contractsMode>
      <contractsRepositoryUrl>https://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>
前面列出的Maven插件中的许多值都可以进行更改。我们将其包含在内是为了说明目的,而非试图提供一个“典型”的示例。

5.5.2。对于Gradle项目

要使用 Gradle 项目:spring-doc.cadn.net.cn

  1. 添加一个自定义配置以用于常见的仓库依赖,如下所示:spring-doc.cadn.net.cn

    ext {
        contractsGroupId = "com.example"
        contractsArtifactId = "common-repo"
        contractsVersion = "1.2.3"
    }
    
    configurations {
        contracts {
            transitive = false
        }
    }
  2. 将通用仓库依赖项添加到您的类路径中,如下所示:spring-doc.cadn.net.cn

    dependencies {
        contracts "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
        testCompile "${contractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    }
  3. 将依赖项下载到适当的文件夹中,如下所示:spring-doc.cadn.net.cn

    task getContracts(type: Copy) {
        from configurations.contracts
        into new File(project.buildDir, "downloadedContracts")
    }
  4. 解压 JAR 文件,如下所示:spring-doc.cadn.net.cn

    task unzipContracts(type: Copy) {
        def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar")
        def outputDir = file("${buildDir}/unpackedContracts")
    
        from zipTree(zipFile)
        into outputDir
    }
  5. 清理未使用的合约,具体如下:spring-doc.cadn.net.cn

    task deleteUnwantedContracts(type: Delete) {
        delete fileTree(dir: "${buildDir}/unpackedContracts",
            include: "**/*",
            excludes: [
                "**/${project.name}/**"",
                "**/${first-topic}/**",
                "**/${second-topic}/**"])
    }
  6. 创建任务依赖关系,如下所示:spring-doc.cadn.net.cn

    unzipContracts.dependsOn("getContracts")
    deleteUnwantedContracts.dependsOn("unzipContracts")
    build.dependsOn("deleteUnwantedContracts")
  7. 通过指定包含契约的目录来配置插件,方法是设置 contractsDslDir 属性,如下所示:spring-doc.cadn.net.cn

    contracts {
        contractsDslDir = new File("${buildDir}/unpackedContracts")
    }

6. 如何将Git用作合同和桩的存储?

在多语言的世界中,有些语言不使用二进制存储,例如 Artifactory 或 Nexus。从 Spring Cloud Contract 版本 2.0.0 开始,我们提供了将合约和桩存放到 SCM(源代码管理)仓库中的机制。目前,唯一受支持的 SCM 是 Git。spring-doc.cadn.net.cn

仓库必须具有以下设置(您可以从 此处 查看):spring-doc.cadn.net.cn

.
└── 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 文件夹下:spring-doc.cadn.net.cn

  • 我们将应用程序按 groupId 分组(例如 com.example)。spring-doc.cadn.net.cn

  • 每个应用程序由其 artifactId 表示(例如,beer-api-producer-git)。spring-doc.cadn.net.cn

  • 接下来,每个应用按其版本进行组织(例如 0.0.1-SNAPSHOT)。从 Spring Cloud Contract 版本 2.1.0 开始,您可以按如下方式指定版本(假设您的版本遵循语义化版本控制):spring-doc.cadn.net.cn

    • +latest:用于查找您存根(stubs)的最新版本(假设快照版本始终是给定修订号的最新构件)。这意味着:spring-doc.cadn.net.cn

      • 如果您有 1.0.0.RELEASE2.0.0.BUILD-SNAPSHOT2.0.0.RELEASE,我们假定最新的为 2.0.0.BUILD-SNAPSHOTspring-doc.cadn.net.cn

      • 如果您有 1.0.0.RELEASE2.0.0.RELEASE,我们假设最新的是 2.0.0.RELEASEspring-doc.cadn.net.cn

      • 如果您有一个名为 latest+ 的版本,我们将选择该文件夹。spring-doc.cadn.net.cn

    • release: 用于查找您的存根(stubs)的最新发布版本。这意味着:spring-doc.cadn.net.cn

      • 如果您有 1.0.0.RELEASE2.0.0.BUILD-SNAPSHOT2.0.0.RELEASE,我们假定最新的是 2.0.0.RELEASEspring-doc.cadn.net.cn

      • 如果您有一个名为 release 的版本,我们将选择该文件夹。spring-doc.cadn.net.cn

最后,还有两个文件夹:spring-doc.cadn.net.cn

  • contracts: 最佳实践是将每个消费者所需的契约存储在以消费者名称命名的文件夹中(例如 beer-api-consumer)。这样,您就可以使用 stubs-per-consumer 功能。后续的目录结构是任意的。spring-doc.cadn.net.cn

  • mappings: Maven 或 Gradle Spring Cloud Contract 插件将存根服务器映射推送到此文件夹。在消费者端,Stub Runner 会扫描此文件夹以启动带有存根定义的存根服务器。该文件夹结构是复制了在 contracts 子文件夹中创建的结构。spring-doc.cadn.net.cn

6.1. 协议约定

要控制契约的来源类型和位置(是二进制存储还是 SCM 仓库),您可使用仓库 URL 中的协议。Spring Cloud Contract 会遍历已注册的协议解析器,并尝试通过插件获取契约(contract),或从 Stub Runner 获取存根(stubs)。spring-doc.cadn.net.cn

对于 SCM 功能,目前我们支持 Git 仓库。要使用它,在需要放置仓库 URL 的属性中,您必须在连接 URL 前加上 git://。以下列表展示了一些示例:spring-doc.cadn.net.cn

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

6.2. Producer

对于生产者而言,若要使用 SCM(源代码管理)方法,我们可以复用用于外部契约的相同机制。我们通过将 Spring Cloud Contract 配置为使用从以 git:// 协议开头的 URL 获取的 SCM 实现来实现这一点。spring-doc.cadn.net.cn

您必须在 Maven 中手动添加 pushStubsToScm 目标,或在 Gradle 中执行(绑定)pushStubsToScm任务。我们不会将桩文件推送到您的 Git 仓库的 origin

以下列表包含了Maven和Gradle构建文件中的相关部分:spring-doc.cadn.net.cn

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")

还可以进一步自定义publishStubsToScm Gradle任务。在下面的例子中,任务被定制为从本地 Git 仓库中选择合同:spring-doc.cadn.net.cn

Gradle
publishStubsToScm {
    // We want to modify the default set up of the plugin when publish stubs to scm is called
    // 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://${new File(project.rootDir, "../target")}/contract_empty_git/"
    }
    // We set the contracts mode to `LOCAL`
    contractsMode = "LOCAL"
    }
重要

2.3.0.RELEASE 开始,先前用于 customize{} 自定义的 publishStubsToScm 闭包不再可用。设置应直接应用于 publishStubsToScm 闭包中,如上例所示。spring-doc.cadn.net.cn

采用这种配置:spring-doc.cadn.net.cn

6.3. 将合约存储在本地的生产者

另一种使用 SCM 作为存根和契约目标的方法是将契约本地存储在生产者中,然后仅将合约和存根推送到 SCM。 以下清单显示了使用 Maven 和 Gradle 实现此设置所需的步骤:spring-doc.cadn.net.cn

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://[email protected]: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
    // 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://${new File(project.rootDir, "../target")}/contract_empty_git/"
    }
    // The mode can't be classpath
    contractsMode = "LOCAL"
    }
}

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

采用这种配置:spring-doc.cadn.net.cn

6.4. 将合同与生产者和存根保存在外部存储库中

您还可以将契约保留在生产者仓库中,但将存根存储在外部 Git 仓库中。这在您希望使用基础的消费者-生产者协作流程,但无法使用构件仓库来存储存根时最为有用。spring-doc.cadn.net.cn

为此,请使用常规的生产者设置,然后添加 pushStubsToScm 目标,并将 contractsRepositoryUrl 设置为存放存根的仓库。spring-doc.cadn.net.cn

6.5. 消费者

在消费者端,传递repositoryRoot参数时,无论是从@AutoConfigureStubRunner注解、JUnit规则、JUnit 5扩展还是属性中传入,都可以通过git://协议前缀来指定SCM仓库的URL。下面的例子展示了如何操作:spring-doc.cadn.net.cn

@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"
)

采用这种配置:spring-doc.cadn.net.cn

7. 我如何使用契约代理?

使用Pact时,可以使用Pact Broker存储和共享Pact定义。从Spring Cloud Contract 2.0.0开始,您可以从Pact Broker获取Pact文件以生成 测试和桩。spring-doc.cadn.net.cn

<pact遵循消费者契约约定。这意味着消费者首先创建pact定义,然后与生产者共享这些文件。这些预期来自消费者的代码,如果未满足这些预期,则可能会破坏生产者。</pact>

7.1. 如何使用 Pact

Spring Cloud Contract 包括支持 for the Pact representation of contracts up until version 4. Instead of using the DSL, you can use Pact files. In this section, we show how to add Pact support for your project. Note, however, that not all functionality is supported. Starting with version 3, you can combine multiple matchers for the same element; you can use matchers for the body, headers, request and path; and you can use value generators. Spring Cloud Contract currently only supports multiple matchers that are combined by using the AND rule logic. Next to that, the request and path matchers are skipped during the conversion. When using a date, time, or datetime value generator with a given format, the given format is skipped and the ISO format is used.spring-doc.cadn.net.cn

7.2. 协约转换器

为了正确支持使用 Pact 的 Spring Cloud Contract 消息传递方式,您必须提供一些附加的元数据条目。spring-doc.cadn.net.cn

要定义消息发送的目标,您必须在 Pact 文件中设置一个metaData条目,并将sentTo键设置为要发送到的消息目标(例如,"metaData": { "sentTo": "activemq:output" })。spring-doc.cadn.net.cn

7.3. 断言契约

Spring Cloud Contract 可以读取 Pact JSON 定义。您可以将文件放在src/test/resources/contracts文件夹中。记得将spring-cloud-contract-pact依赖项添加到您的类路径中。下面的例子展示了这样的 Pact 合同:spring-doc.cadn.net.cn

{
  "provider": {
    "name": "Provider"
  },
  "consumer": {
    "name": "Consumer"
  },
  "interactions": [
    {
      "description": "",
      "request": {
        "method": "PUT",
        "path": "/pactfraudcheck",
        "headers": {
          "Content-Type": "application/json"
        },
        "body": {
          "clientId": "1234567890",
          "loanAmount": 99999
        },
        "generators": {
          "body": {
            "$.clientId": {
              "type": "Regex",
              "regex": "[0-9]{10}"
            }
          }
        },
        "matchingRules": {
          "header": {
            "Content-Type": {
              "matchers": [
                {
                  "match": "regex",
                  "regex": "application/json.*"
                }
              ],
              "combine": "AND"
            }
          },
          "body": {
            "$.clientId": {
              "matchers": [
                {
                  "match": "regex",
                  "regex": "[0-9]{10}"
                }
              ],
              "combine": "AND"
            }
          }
        }
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": {
          "fraudCheckStatus": "FRAUD",
          "rejection.reason": "Amount too high"
        },
        "matchingRules": {
          "header": {
            "Content-Type": {
              "matchers": [
                {
                  "match": "regex",
                  "regex": "application/json.*"
                }
              ],
              "combine": "AND"
            }
          },
          "body": {
            "$.fraudCheckStatus": {
              "matchers": [
                {
                  "match": "regex",
                  "regex": "FRAUD"
                }
              ],
              "combine": "AND"
            }
          }
        }
      }
    }
  ],
  "metadata": {
    "pact-specification": {
      "version": "3.0.0"
    },
    "pact-jvm": {
      "version": "3.5.13"
    }
  }
}

7.4 提供者契约<br/>

在生产者方面,您必须向插件配置中添加两个额外的依赖项。一个是Spring Cloud Contract Pact支持,另一个是您使用的当前Pact版本。下面列出的内容展示了如何针对Maven和Gradle进行操作:spring-doc.cadn.net.cn

Maven
Gradle
// if additional dependencies are needed e.g. for Pact
classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}"

执行应用程序构建时,会生成一个测试和桩文件。以下示例显示了由此过程产生的测试和桩:spring-doc.cadn.net.cn

测试
@Test
    public void validate_shouldMarkClientAsFraud() throws Exception {
        // given:
            MockMvcRequestSpecification request = given()
                    .header("Content-Type", "application/vnd.fraud.v1+json")
                    .body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");

        // when:
            ResponseOptions response = given().spec(request)
                    .put("/fraudcheck");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
            assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
        // and:
            assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
    }
占位符
{
  "id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
  "uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
  "request" : {
    "url" : "/fraudcheck",
    "method" : "PUT",
    "headers" : {
      "Content-Type" : {
        "matches" : "application/vnd\\.fraud\\.v1\\+json.*"
      }
    },
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$[?(@.['loanAmount'] = 99999)]"
    }, {
      "matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
    } ]
  },
  "response" : {
    "status" : 200,
    "body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
    "headers" : {
      "Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
    },
    "transformers" : [ "response-template" ]
  },
}

7.5. 消费者契约

在消费者端,您必须向项目中添加两个额外的依赖项。一个是Spring Cloud Contract Pact支持,另一个是您使用的当前Pact版本。下面的列表显示了如何同时为Maven和Gradle执行此操作:spring-doc.cadn.net.cn

Maven
Gradle

7.6. 与 Pact Broker 通信

repositoryRoot属性以Pact协议开头时(以pact://开头),存根下载器会尝试从Pact Broker获取Pact合同定义。
无论设置为pact://之后的内容都会被解析为Pact Broker URL。spring-doc.cadn.net.cn

通过设置环境变量、系统属性或插件或合同仓库配置中设置的属性,您可以调整下载器的行为。下表描述了这些属性:spring-doc.cadn.net.cn

表1. 断言存根下载器属性

属性名称spring-doc.cadn.net.cn

默认spring-doc.cadn.net.cn

描述spring-doc.cadn.net.cn

* pactbroker.host (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.host (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_HOST (env prop)spring-doc.cadn.net.cn

从传递给repositoryRoot的URL获取主机spring-doc.cadn.net.cn

契约代理的URL。spring-doc.cadn.net.cn

* pactbroker.port (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.port (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_PORT (env prop)spring-doc.cadn.net.cn

从传递给 repositoryRoot 的 URL 导入spring-doc.cadn.net.cn

契约代理的端口。spring-doc.cadn.net.cn

* pactbroker.protocol (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.protocol (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_PROTOCOL (env prop)spring-doc.cadn.net.cn

从URL传递到repositoryRoot的协议spring-doc.cadn.net.cn

契约经纪人的协议。spring-doc.cadn.net.cn

* pactbroker.tags (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.tags (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_TAGS (env prop)spring-doc.cadn.net.cn

桩版本,或如果版本为+则为latestspring-doc.cadn.net.cn

应使用的标记来获取存根。spring-doc.cadn.net.cn

* pactbroker.auth.scheme (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.auth.scheme (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_SCHEME (env prop)spring-doc.cadn.net.cn

Basicspring-doc.cadn.net.cn

连接到Pact Broker时应使用的身份验证类型。spring-doc.cadn.net.cn

* pactbroker.auth.username (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.auth.username (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_USERNAME (env prop)spring-doc.cadn.net.cn

传递给contractsRepositoryUsername(maven)或contractRepository.username(gradle)的用户名spring-doc.cadn.net.cn

连接到Pact Broker时使用的用户名。spring-doc.cadn.net.cn

* pactbroker.auth.password (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.auth.password (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD (env prop)spring-doc.cadn.net.cn

传递给contractsRepositoryPassword(maven)或contractRepository.password(gradle)的密码spring-doc.cadn.net.cn

连接到Pact Broker时使用的密码。spring-doc.cadn.net.cn

* pactbroker.provider-name-with-group-id (plugin prop)spring-doc.cadn.net.cn

* stubrunner.properties.pactbroker.provider-name-with-group-id (system prop)spring-doc.cadn.net.cn

* STUBRUNNER_PROPERTIES_PACTBROKER_PROVIDER_NAME_WITH_GROUP_ID (env prop)spring-doc.cadn.net.cn

falsespring-doc.cadn.net.cn

true时,提供者名称是groupId:artifactId的组合。如果为false,则仅使用artifactIdspring-doc.cadn.net.cn

7.7. 流程:使用Pact Broker的消费者契约方法 | 消费者端

消费者使用 Pact 框架生成 Pact 文件。这些 Pact 文件被发送到 Pact Broker。你可以在此找到此类设置的示例这里spring-doc.cadn.net.cn

7.8. 流程:在生产者端使用Pact Broker的消费者契约方法

对于生产者使用契约代理中的契约文件,我们可以复用用于外部契约的相同机制。我们将Spring Cloud Contract路由到使用包含pact://协议的URL的Pact实现。您可以将该URL传递给Pact代理。您可以在此处找到这种设置的一个示例。
下面列出的是Maven和Gradle的配置详情:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

7.9. 流程:生产者契约方法,Pact在消费者端使用

在你不想使用消费者契约方法(为每个单独的消费者定义期望)但更倾向于使用生产者契约(由生产者提供契约并发布存根)的情况下,可以使用带有存根运行器选项的 Spring Cloud Contract。你可以在这里找到这种设置的例子。spring-doc.cadn.net.cn

请记得添加 Stub Runner 和 Spring Cloud Contract Pact 模块作为测试依赖项。spring-doc.cadn.net.cn

以下清单显示了Maven和Gradle的配置详细信息:spring-doc.cadn.net.cn

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 传递给repositoryRoot,前面加上pact://协议(例如,pact://http://localhost:8085),如下所示:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

8. 我该如何调试生成的测试客户端发送的请求/响应?

生成的测试在某种程度上都简化为RestAssured。RestAssured依赖于Apache HttpClient。HttpClient有一个叫做wire logging的功能,它可以记录整个请求和响应到HttpClient中。Spring Boot有logging的公共应用程序属性来完成这种操作。要使用它,请按照以下方式将其添加到您的应用程序属性中:spring-doc.cadn.net.cn

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

9. 如何调试通过 WireMock 发送的映射、请求或响应?

从版本 1.2.0 开始,我们启用 WireMock 日志记录到info 并设置 WireMock 通知器为详细模式。现在您可以准确地知道 WireMock 服务器收到了哪些请求以及选择了哪个匹配的响应定义。spring-doc.cadn.net.cn

要关闭此功能,请将 WireMock 日志记录设置为 ERROR,如下所示:spring-doc.cadn.net.cn

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

10. 我如何查看在HTTP服务器存根中注册了什么?

您可以使用mappingsOutputFolder属性在@AutoConfigureStubRunnerStubRunnerRuleStubRunnerExtension上按构件ID转储所有映射。还附加了给定存根服务器启动时所使用的端口。spring-doc.cadn.net.cn

11. 我该如何引用文件中的文本?

在版本 1.2.0 中,我们添加了此功能。您可以在 DSL 中调用 file(…​) 方法,并提供相对于契约位置的路径。如果您使用 YAML,可以使用 bodyFromFile 属性。spring-doc.cadn.net.cn

12. 如何从 Spring Cloud Contract Contracts 生成 Pact、YAML 或 X 文件?

Spring Cloud Contract 提供了一个 ToFileContractsTransformer 类,该类允许您将合约转储为文件,以供给定的 ContractConverter 使用。它包含一个 static void main 方法,使您可以作为可执行程序运行转换器。它接受以下参数:spring-doc.cadn.net.cn

  • 参数 1: FQNContractConverter 的完整限定名(例如,PactContractConverter)。必填spring-doc.cadn.net.cn

  • 参数 2: path:已转储文件应存储的路径。 可选 —— 默认为 target/converted-contractsspring-doc.cadn.net.cn

  • 参数 3: path:应搜索合约的路径。 可选 —— 默认值为 src/test/resources/contractsspring-doc.cadn.net.cn

执行转换器后,Spring Cloud Contract 文件会被处理,并根据提供的ContractTransformer的完整类名(FQN),将合同转换为所需的格式并转储到提供的文件夹中。spring-doc.cadn.net.cn

下面的例子展示了如何为Maven和Gradle配置Pact集成:spring-doc.cadn.net.cn

Maven
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.6.0</version>
    <executions>
        <execution>
            <id>convert-dsl-to-pact</id>
            <phase>process-test-classes</phase>
            <configuration>
                <classpathScope>test</classpathScope>
                <mainClass>
                    org.springframework.cloud.contract.verifier.util.ToFileContractsTransformer
                </mainClass>
                <arguments>
                    <argument>
                        org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter
                    </argument>
                    <argument>${project.basedir}/target/pacts</argument>
                    <argument>
                        ${project.basedir}/src/test/resources/contracts
                    </argument>
                </arguments>
            </configuration>
            <goals>
                <goal>java</goal>
            </goals>
        </execution>
    </executions>
</plugin>
Gradle
task convertContracts(type: JavaExec) {
    main = "org.springframework.cloud.contract.verifier.util.ToFileContractsTransformer"
    classpath = sourceSets.test.compileClasspath
    args("org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter",
            "${project.rootDir}/build/pacts", "${project.rootDir}/src/test/resources/contracts")
}

test.dependsOn("convertContracts")

13. 我该如何处理传递依赖项?

Spring Cloud Contract 插件会为您创建存档桩。一个出现的问题是,当重用这些桩时,您可能会错误地导入所有那些桩依赖项。在构建Maven构件时,即使您有几个不同的JAR文件,它们都共享一个pom,如下列表所示:spring-doc.cadn.net.cn

├── producer-0.0.1.BUILD-20160903.075506-1-stubs.jar
├── producer-0.0.1.BUILD-20160903.075506-1-stubs.jar.sha1
├── producer-0.0.1.BUILD-20160903.075655-2-stubs.jar
├── producer-0.0.1.BUILD-20160903.075655-2-stubs.jar.sha1
├── producer-0.0.1.BUILD-SNAPSHOT.jar
├── producer-0.0.1.BUILD-SNAPSHOT.pom
├── producer-0.0.1.BUILD-SNAPSHOT-stubs.jar
├── ...
└── ...

有三种处理这些依赖项的方法,以避免与传递依赖项相关的问题:spring-doc.cadn.net.cn

13.1 如何将所有应用依赖项标记为可选?

如果在producer应用程序中,您将所有依赖项标记为可选的,那么当您在另一个应用程序中包含producer存根(或当该依赖项被Stub Runner下载时),由于所有依赖项都是可选的,因此它们不会被下载。spring-doc.cadn.net.cn

13.2 如何为桩(Stubs)创建一个独立的artifactid

如果您创建一个独立的 artifactid,您可以按自己希望的方式进行配置。例如,您可能决定完全不依赖任何其他组件。spring-doc.cadn.net.cn

13.3 如何在消费者端排除依赖?

作为消费者,如果您将存根依赖项添加到类路径中,您可以显式排除不需要的依赖项。spring-doc.cadn.net.cn

14. 如何从契约生成 Spring REST 文档片段?

当您希望使用 Spring REST Docs 包含 API 的请求和响应时,如果您正在使用 MockMvc 和 RestAssuredMockMvc,只需对您的设置进行一些小幅修改即可。为此,请包含以下依赖项(如果您尚未这样做):spring-doc.cadn.net.cn

Maven
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <optional>true</optional>
</dependency>
Gradle
testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

接下来,您需要对基础类进行一些修改。以下示例使用 WebAppContext 和独立模式(standalone)与 RestAssured 结合:spring-doc.cadn.net.cn

WebAppContext
package com.example.fraud;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public abstract class FraudBaseWithWebAppSetup {

    private static final String OUTPUT = "target/generated-snippets";

    @Rule
    public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);

    @Rule
    public TestName testName = new TestName();

    @Autowired
    private WebApplicationContext context;

    @Before
    public void setup() {
        RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(this.context)
                .apply(documentationConfiguration(this.restDocumentation))
                .alwaysDo(document(
                        getClass().getSimpleName() + "_" + testName.getMethodName()))
                .build());
    }

    protected void assertThatRejectionReasonIsNull(Object rejectionReason) {
        assert rejectionReason == null;
    }

}
独立运行
package com.example.fraud;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestName;

import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;

public abstract class FraudBaseWithStandaloneSetup {

    private static final String OUTPUT = "target/generated-snippets";

    @Rule
    public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);

    @Rule
    public TestName testName = new TestName();

    @Before
    public void setup() {
        RestAssuredMockMvc.standaloneSetup(MockMvcBuilders
                .standaloneSetup(new FraudDetectionController())
                .apply(documentationConfiguration(this.restDocumentation))
                .alwaysDo(document(
                        getClass().getSimpleName() + "_" + testName.getMethodName())));
    }

}
您无需为生成的代码片段指定输出目录(自 Spring REST Docs 1.2.0.RELEASE 版本起)。

15. 如何从指定位置使用存根

如果您想从给定位置获取合约或桩文件而无需克隆仓库或获取JAR,请在提供Stub Runner或Spring Cloud Contract插件的仓库根参数时使用stubs://协议。您可以在文档的此部分中了解更多信息。spring-doc.cadn.net.cn

16. 如何在运行时生成存根

如果您想在运行时为合约生成桩代码,只需切换 generateStubs 注解中的 @AutoConfigureStubRunner 属性,或者调用 JUnit Rule 或 Extension 上的 withGenerateStubs(true) 方法即可。有关更多信息,请参阅文档中的本节spring-doc.cadn.net.cn

17. 如果没有合约或存根,我该如何让构建通过

如果您希望 Stub Runner 在未找到存根时不失败,只需在 @AutoConfigureStubRunner 注解中切换 generateStubs 属性,或者在 JUnit 规则或扩展上调用 withFailOnNoStubs(false) 方法即可。有关此问题的更多信息,请参阅文档中的 本节spring-doc.cadn.net.cn

如果您希望插件在未找到契约时不会导致构建失败,可以在 Maven 中设置 failOnNoStubs 标志,或在 Gradle 中调用 contractRepository { failOnNoStubs(false) } 闭包。spring-doc.cadn.net.cn

18. 如何标记合同正在进行中

如果合同正在进行中,则表示不会在生产者端生成测试,但会生成存根。您可以在文档的此部分阅读有关更多信息。spring-doc.cadn.net.cn

在持续集成构建中,发布到生产环境之前,您希望确保类路径上不存在正在进行的合约。这是因为如果没有这样做可能会导致错误的结果。因此,默认情况下,在Spring Cloud Contract插件中我们将failOnInProgress设置为true。如果您想允许在这种情况下生成测试,则只需将标志设置为false即可。spring-doc.cadn.net.cn