本节更详细地介绍如何使用 Spring Cloud Contract。它涵盖了诸如如何使用 Spring Cloud Contract 的流程等主题。我们还介绍了一些 Spring Cloud Contract 最佳实践。

如果您开始使用 Spring Cloud Contract,您可能应该 在深入本节之前阅读入门指南。

1. 使用 Nexus 或 Artifactory 中的存根进行提供商合约测试

您可以检查开发您的第一个基于 Spring Cloud 合约的应用程序链接,以查看使用 Nexus 或 Artifactory 流程中的存根进行的提供商合约测试。

2. 使用 Git 中的存根进行提供商合同测试

在此流程中,我们执行提供者合约测试(生产者不知道消费者如何使用其 API)。存根上传到单独的存储库(它们不会上传到 Artifactory 或 Nexus)。

2.1. 先决条件

在使用 git 中的存根测试提供商合约之前,您必须提供一个包含每个生产者的所有存根的 git 存储库。有关此类项目的示例,请参阅 此示例此示例。由于将存根推送到那里,存储库具有以下结构:

$ tree .
└── META-INF
   └── folder.with.group.id.as.its.name
       └── folder-with-artifact-id
           └── folder-with-version
               ├── contractA.groovy
               ├── contractB.yml
               └── contractC.groovy

您还必须提供已设置 Spring Cloud Contract Stub Runner 的消费者代码。有关此类项目的示例,请参阅此示例并搜索测试 BeerControllerGitTest。您还必须提供已设置 Spring Cloud Contract 的生产者代码以及插件。有关此类项目的示例,请参阅 此示例

2.2. 流动

该流程看起来与开发第一个基于 Spring Cloud 合约的应用程序中介绍的流程完全相同 ,但Stub Storage实现是一个 git 存储库。

您可以在文档的“操作方法”页面中阅读有关设置 git 存储库以及设置消费者和生产者端的更多信息。

2.3. 消费者设置

为了从 git 存储库而不是 Nexus 或 Artifactory 获取存根,您需要在 Stub Runner 中的属性gitURL 中使用协议。repositoryRoot以下示例展示了如何设置:

注解
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        repositoryRoot = "git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git",
        ids = "com.example:artifact-id:0.0.1")
JUnit 4 规则
@Rule
    public StubRunnerRule rule = new StubRunnerRule()
            .downloadStub("com.example","artifact-id", "0.0.1")
            .repoRoot("git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE);
JUnit 5 扩展
@RegisterExtension
    public StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
            .downloadStub("com.example","artifact-id", "0.0.1")
            .repoRoot("git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE);

2.4. 设置生产者

要将存根推送到 git 存储库而不是 Nexus 或 Artifactory,您需要使用git插件设置的 URL 中的协议。此外,您还需要明确告诉插件在构建过程结束时推送存根。以下示例展示了如何在 Maven 和 Gradle 中执行此操作:

梅文
<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://git://git@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://git://git@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 run
*/
publish.dependsOn("publishStubsToScm")

您可以在文档的“操作方法”部分中阅读有关设置 git 存储库的更多信息 。

4. 消费者驱动的合约以及外部存储库中的合约

在此流程中,我们执行消费者驱动的合同测试。合同定义存储在单独的存储库中。

4.1. 先决条件

要将消费者驱动的合约与外部存储库中保存的合约一起使用,您需要设置一个 git 存储库:

  • 包含每个生产者的所有合同定义。

  • 可以将合约定义打包在 JAR 中。

  • pom.xml对于每个合约生产者,包含一种通过 Spring Cloud Contract 插件(SCC 插件)在本地安装存根的方法(例如, )。

有关更多信息,请参阅“操作方法”部分,我们在其中描述了如何设置此类存储库。有关此类项目的示例,请参阅此示例

您还需要设置了 Spring Cloud Contract Stub Runner 的消费者代码。有关此类项目的示例,请参阅此示例。您还需要设置了 Spring Cloud Contract 的生产者代码以及插件。有关此类项目的示例,请参阅此示例。存根存储是 Nexus 或 Artifactory。

从高层次来看,流程如下:

  1. 消费者使用来自单独存储库的合同定义。

  2. 一旦消费者的工作完成,就会在消费者端创建一个包含工作代码的分支,并向保存合约定义的单独存储库发出拉取请求。

  3. 生产者将拉取请求接管到具有合约定义的单独存储库,并在本地安装包含所有合约的 JAR。

  4. 生产者从本地存储的 JAR 生成测试并编写缺少的实现以使测试通过。

  5. 一旦生产者的工作完成,对保存合约定义的存储库的拉取请求就会被合并。

  6. CI 工具使用合约定义构建存储库并将包含合约定义的 JAR 上传到 Nexus 或 Artifactory 后,生产者可以合并其分支。

  7. 最后,消费者可以切换到在线工作,从远程位置获取生产者的存根,并且分支可以合并到master。

4.2. 消费流程

消费者:

  1. 编写一个向生产者发送请求的测试。

    由于不存在服务器,测试失败。

  2. 克隆保存合约定义的存储库。

  3. 将需求设置为文件夹下的合同,并将消费者名称设置为生产者的子文件夹。

    例如,对于名为 的生产者producer和名为 的消费者consumer,合约将存储在src/main/resources/contracts/producer/consumer/)

  4. 定义合约后,将生产者存根安装到本地存储,如以下示例所示:

    $ cd src/main/resource/contracts/producer
    $ ./mvnw clean install
  5. 在消费者测试中设置 Spring Cloud Contract (SCC) Stub Runner,以便:

    • 从本地存储中获取生产者存根。

    • 在每个消费者存根模式下工作(这启用了消费者驱动的合同模式)。

      SCC 存根运行器:

    • 获取生产者存根。

    • 使用生产者存根运行内存中 HTTP 服务器存根。现在您的测试与 HTTP 服务器存根进行通信,并且您的测试通过了。

    • 使用合约定义和生产者的新合约创建对存储库的拉取请求。

    • 对您的消费者代码进行分支,直到生产者团队合并他们的代码。

下面的 UML 图显示了消费者流程:

流程概览 消费者 CDC 外部消费者

4.3. 生产者流程

制片人:

  1. 使用合约定义接管对存储库的拉取请求。您可以从命令行执行此操作,如下所示

    $ git checkout -b the_branch_with_pull_request master
    git pull https://github.com/user_id/project_name.git the_branch_with_pull_request
  2. 安装合约定义,如下

    $ ./mvnw clean install
  3. 设置插件从 JAR 而不是从 获取合约定义 src/test/resources/contracts,如下所示:

    梅文
    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>${spring-cloud-contract.version}</version>
        <extensions>true</extensions>
        <configuration>
            <!-- We want to use the JAR with contracts with the following coordinates -->
            <contractDependency>
                <groupId>com.example</groupId>
                <artifactId>beer-contracts</artifactId>
            </contractDependency>
            <!-- The JAR with contracts should be taken from Maven local -->
            <contractsMode>LOCAL</contractsMode>
            <!-- ... additional configuration -->
        </configuration>
    </plugin>
    摇篮
    contracts {
        // We want to use the JAR with contracts with the following coordinates
        // group id `com.example`, artifact id `beer-contracts`, LATEST version and NO classifier
        contractDependency {
            stringNotation = 'com.example:beer-contracts:+:'
        }
        // The JAR with contracts should be taken from Maven local
        contractsMode = "LOCAL"
        // Additional configuration
    }
    
  4. 运行构建以生成测试和存根,如下所示:

    梅文
    ./mvnw clean install
    摇篮
    ./gradlew clean build
    
  5. 编写缺少的实现,以使测试通过。

  6. 将存储库的拉取请求与合约定义合并,如下所示:

    $ git commit -am "Finished the implementation to make the contract tests pass"
    $ git checkout master
    $ git merge --no-ff the_branch_with_pull_request
    $ git push origin master

    CI 系统使用合约定义构建项目,并将包含合约定义的 JAR 上传到 Nexus 或 Artifactory。

  7. 切换到远程工作。

  8. 设置插件,以便不再从本地存储而是从远程位置获取合约定义,如下所示:

    梅文
    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>${spring-cloud-contract.version}</version>
        <extensions>true</extensions>
        <configuration>
            <!-- We want to use the JAR with contracts with the following coordinates -->
            <contractDependency>
                <groupId>com.example</groupId>
                <artifactId>beer-contracts</artifactId>
            </contractDependency>
            <!-- The JAR with contracts should be taken from a remote location -->
            <contractsMode>REMOTE</contractsMode>
            <!-- ... additional configuration -->
        </configuration>
    </plugin>
    摇篮
    contracts {
        // We want to use the JAR with contracts with the following coordinates
        // group id `com.example`, artifact id `beer-contracts`, LATEST version and NO classifier
        contractDependency {
            stringNotation = 'com.example:beer-contracts:+:'
        }
        // The JAR with contracts should be taken from a remote location
        contractsMode = "REMOTE"
        // Additional configuration
    }
    
  9. 将生产者代码与新的实现合并。

  10. CI系统:

    • 构建项目。

    • 生成测试、存根和存根 JAR。

    • 将工件与应用程序和存根一起上传到 Nexus 或 Artifactory。

下面的 UML 图显示了生产者流程:

流程概览 消费者 CDC 外部生产者

5. 消费者驱动的合约与生产者端的合约,推送到 Git

您可以阅读带有生产者端合约的消费者驱动合约 (CDC) 分步指南,以查看带有生产者端合约的消费者驱动合约流程。

存根存储实现是一个 git 存储库。我们 在 Git 中使用存根进行提供商合同测试部分描述了它的设置。

您可以在文档的“操作方法”部分中阅读有关为消费者和生产者端设置 git 存储库的更多信息。

6. 在 Artifactory 中使用存根对非 Spring 应用程序进行提供者合约测试

6.1. 流动

您可以阅读开发您的第一个基于 Spring Cloud 合约的应用程序,以了解使用 Nexus 或 Artifactory 中的存根进行提供商合约测试的流程。

6.2. 设置消费者

对于消费者端,您可以使用 JUnit 规则。这样,您就不需要启动 Spring 上下文。下面的清单显示了这样的规则(在 JUnit4 和 JUnit 5 中);

JUnit 4 规则
@Rule
    public StubRunnerRule rule = new StubRunnerRule()
            .downloadStub("com.example","artifact-id", "0.0.1")
            .repoRoot("git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE);
JUnit 5 扩展
@RegisterExtension
    public StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
            .downloadStub("com.example","artifact-id", "0.0.1")
            .repoRoot("git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE);

6.3. 设置生产者

默认情况下,Spring Cloud Contract 插件使用 Rest Assured 的MockMvc设置来生成测试。由于非 Spring 应用程序不使用MockMvc,因此您可以更改 testModetoEXPLICIT以将真实请求发送到绑定在特定端口的应用程序。

在此示例中,我们使用名为Javalin的框架来启动非 Spring HTTP 服务器。

假设我们有以下应用程序:

package com.example.demo;

import io.javalin.Javalin;

public class DemoApplication {

    public static void main(String[] args) {
        new DemoApplication().run(7000);
    }

    public Javalin start(int port) {
        return Javalin.create().start(port);
    }

    public Javalin registerGet(Javalin app) {
        return app.get("/", ctx -> ctx.result("Hello World"));
    }

    public Javalin run(int port) {
        return registerGet(start(port));
    }

}

给定该应用程序,我们可以设置插件以使用该EXPLICIT模式(即将请求发送到真实端口),如下所示:

梅文
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <baseClassForTests>com.example.demo.BaseClass</baseClassForTests>
        <!-- This will setup the EXPLICIT mode for the tests -->
        <testMode>EXPLICIT</testMode>
    </configuration>
</plugin>
摇篮
contracts {
    // This will setup the EXPLICIT mode for the tests
    testMode = "EXPLICIT"
    baseClassForTests = "com.example.demo.BaseClass"
}

基类可能类似于以下内容:

import io.javalin.Javalin;
import io.restassured.RestAssured;
import org.junit.After;
import org.junit.Before;
import org.springframework.cloud.test.TestSocketUtils;

public class BaseClass {

    Javalin app;

    @Before
    public void setup() {
        // pick a random port
        int port = TestSocketUtils.findAvailableTcpPort();
        // start the application at a random port
        this.app = start(port);
        // tell Rest Assured where the started application is
        RestAssured.baseURI = "http://localhost:" + port;
    }

    @After
    public void close() {
        // stop the server after each test
        this.app.stop();
    }

    private Javalin start(int port) {
        // reuse the production logic to start a server
        return new DemoApplication().run(port);
    }
}

通过这样的设置:

  • 我们已经设置了 Spring Cloud Contract 插件来使用该EXPLICIT模式发送真实请求而不是模拟请求。

  • 我们定义了一个基类:

    • 在每次测试的随机端口上启动 HTTP 服务器。

    • 设置放心以将请求发送到该端口。

    • 每次测试后关闭 HTTP 服务器。

7. 在非 JVM 世界中使用 Artifactory 中的存根进行提供商合约测试

在此流程中,我们假设:

  • API 生产者和 API 消费者是非 JVM 应用程序。

  • 合约定义是用 YAML 编写的。

  • 存根存储是 Artifactory 或 Nexus。

  • 使用 Spring Cloud Contract Docker (SCC Docker) 和 Spring Cloud Contract Stub Runner Docker (SCC Stub Runner Docker) 镜像。

您可以在此处阅读有关如何将 Spring Cloud Contract 与 Docker 结合使用的更多信息。

在这里,您可以阅读有关如何在多语言世界中使用 Spring Cloud Contract 的博客文章。

在这里,您可以找到一个使用 Spring Cloud Contract 作为生产者和消费者的 NodeJS 应用程序示例。

7.1. 生产者流程

在较高层面上,生产者:

  1. 编写合约定义(例如,在 YAML 中)。

  2. 将构建工具设置为:

    1. 在给定端口上使用模拟服务启动应用程序。

      如果无法进行模拟,您可以设置基础设施并以有状态的方式定义测试。

    2. 运行 Spring Cloud Contract Docker 镜像并将正在运行的应用程序的端口作为环境变量传递。

SCC Docker 映像: * 从附加卷生成测试。* 针对正在运行的应用程序运行测试。

测试完成后,存根将上传到存根存储站点(例如 Artifactory 或 Git)。

下面的 UML 图显示了生产者流程:

流提供者非 JVM 生产者

7.2. 消费流程

在较高层面上,消费者:

  1. 将构建工具设置为:

    • 启动 Spring Cloud Contract Stub Runner Docker 映像并启动存根。

      环境变量配置:

    • 要获取的存根。

    • 存储库的位置。

      注意:

    • 要使用本地存储,您还可以将其附加为卷。

    • 需要公开运行存根的端口。

  2. 针对正在运行的存根运行应用程序测试。

下面的 UML 图显示了消费者流程:

流提供者非 JVM 消费者

8. 使用 Nexus 或 Artifactory 中的 REST 文档和存根进行提供商合同测试

在此流程中,我们不使用 Spring Cloud Contract 插件来生成测试和存根。我们编写Spring RESTDocs,并根据它们自动生成存根。最后,我们设置构建来打包存根并将它们上传到存根存储站点 - 在我们的例子中是 Nexus 或 Artifactory。

8.1. 生产者流程

作为生产者,我们:

  1. 为我们的 API 编写 RESTDocs 测试。

  2. 将 Spring Cloud Contract Stub Runner starter 添加到我们的构建 ( spring-cloud-starter-contract-stub-runner) 中,如下所示:

    梅文
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <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>
    摇篮
    dependencies {
        testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
    }
    
    dependencyManagement {
        imports {
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
    }
    
  3. 我们设置构建工具来打包我们的存根,如下所示:

    梅文
    <!-- pom.xml -->
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <executions>
                <execution>
                    <id>stub</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                    <inherited>false</inherited>
                    <configuration>
                        <attach>true</attach>
                        <descriptors>
                            ${basedir}/src/assembly/stub.xml
                        </descriptors>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
    
    <!-- src/assembly/stub.xml -->
    <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>stubs</id>
        <formats>
            <format>jar</format>
        </formats>
        <includeBaseDirectory>false</includeBaseDirectory>
        <fileSets>
            <fileSet>
                <directory>${project.build.directory}/generated-snippets/stubs</directory>
                <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory>
                <includes>
                    <include>**/*</include>
                </includes>
            </fileSet>
        </fileSets>
    </assembly>
    摇篮
    task stubsJar(type: Jar) {
        classifier = "stubs"
        into("META-INF/${project.group}/${project.name}/${project.version}/mappings") {
            include('**/*.*')
            from("${project.buildDir}/generated-snippets/stubs")
        }
    }
    // we need the tests to pass to build the stub jar
    stubsJar.dependsOn(test)
    bootJar.dependsOn(stubsJar)
    

现在,当我们运行测试时,存根会自动发布和打包。

下面的 UML 图显示了生产者流程:

流程提供者 其余文档制作者

8.2. 消费流程

由于消费者流程不受用于生成存根的工具的影响,因此您可以阅读开发第一个基于 Spring Cloud 合约的应用程序,以查看使用 Nexus 或 Artifactory 中的存根进行提供商合约测试的消费者端的流程。

9. 接下来读什么

您现在应该了解如何使用 Spring Cloud Contract 以及您应该遵循的一些最佳实践。您现在可以继续了解特定的 Spring Cloud Contract 功能,或者您可以跳过并阅读Spring Cloud Contract 的高级功能