Contract DSL

Spring Cloud Contract 支持以以下语言编写的 DSL:

  • Groovy

  • YAML

  • Java

  • Kotlin

Spring Cloud Contract 支持在单个文件中定义多个合同(在 Groovy 中,返回列表而不是单个合同)。

以下示例显示契约定义:

Groovy
link:{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SpringTestMethodBodyBuildersSpec.groovy[role=include]
YAML
link:{verifier_root_path}/src/test/resources/yml/contract_rest.yml[role=include]
Java
link:{verifier_root_path}/src/test/resources/contractsToCompile/contract_rest.java[role=include]
Kotlin
link:{verifier_root_path}/src/test/resources/kotlin/contract_rest.kts[role=include]

可以使用以下 standalone Maven 命令将契约编译为存根映射:

mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert

Contract DSL in Groovy

Contract DSL in Groovy

如果你不熟悉 Groovy,请不要担心。也可以在 Groovy DSL 文件中使用 Java 语法。

如果你决定用 Groovy 编写契约,即使你以前从未使用过 Groovy,也不必感到惊慌。实际上并不需要了解该语言,因为 Contract DSL 只使用它的一小部分(仅限于文字、方法调用和闭包)。此外,DSL 是静态类型的,即使不了解 DSL 本身,也可以让程序员阅读。

请记住,在 Groovy 合约文件中,你必须为 Contract 类和 make 静态导入(例如 org.springframework.cloud.spec.Contract.make { …​ })提供限定名称。你还可以为 Contract 类 (import org.springframework.cloud.spec.Contract) 提供导入,然后调用 Contract.make { …​ }

Contract DSL in Java

Contract DSL in Java

要在 Java 中编写契约定义,你需要创建一个类来实现 Supplier<Contract> 接口(用于单个契约)หรือ Supplier<Collection<Contract>>(用于多个契约)。

你也可以在 src/test/java(例如 src/test/java/contracts)下编写契约定义,这样就无需修改项目中的类路径。在这种情况下,你必须向 Spring Cloud Contract 插件提供契约定义的新位置。

以下示例(在 Maven 和 Gradle 中)将在 src/test/java 下提供契约定义:

Maven
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <contractsDirectory>src/test/java/contracts</contractsDirectory>
    </configuration>
</plugin>
Gradle
contracts {
	contractsDslDir = new File(project.rootDir, "src/test/java/contracts")
}

Contract DSL in Kotlin

Contract DSL in Kotlin

要开始用 Kotlin 编写合同,你需要从(新创建的)Kotlin Script 文件(.kts)开始。与 Java DSL 一样,你可以将你的合同放在你选择的任何目录中。默认情况下,Maven 插件将查看 src/test/resources/contracts 目录,Gradle 插件将查看 src/contractTest/resources/contracts 目录。

自 3.0.0 版本起,Gradle 插件也会查看 src/test/resources/contracts 旧版目录以完成迁移。如果在该目录中找到合约,构建期间就会记录警告。

你需要明确地将 spring-cloud-contract-spec-kotlin 依赖项传递给你的项目插件设置。以下示例(在 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>
    <configuration>
        <!-- some config -->
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-spec-kotlin</artifactId>
            <version>${spring-cloud-contract.version}</version>
        </dependency>
    </dependencies>
</plugin>

<dependencies>
        <!-- Remember to add this for the DSL support in the IDE and on the consumer side -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-spec-kotlin</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
Gradle
buildscript {
    repositories {
        // ...
    }
	dependencies {
		classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:$\{scContractVersion}"
	}
}

dependencies {
    // ...

    // Remember to add this for the DSL support in the IDE and on the consumer side
    testImplementation "org.springframework.cloud:spring-cloud-contract-spec-kotlin"
    // Kotlin versions are very particular down to the patch version. The <kotlin_version> needs to be the same as you have imported for your project.
    testImplementation "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:<kotlin_version>"
}

请记住,在 Kotlin 脚本文件中,你必须为 ContractDSL 类提供限定名称。你通常会像这样使用它的 contract 函数:org.springframework.cloud.contract.spec.ContractDsl.contract { …​ }。你还可以为 contract 函数 (import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract) 提供导入,然后调用 contract { …​ }

Contract DSL in YAML

Contract DSL in YAML

要查看 YAML 合同的架构,请访问 YML Schema 页面。

Limitations

Limitations

验证 JSON 数组大小的支持为实验性功能。如果你想启用它,请将以下系统属性的值设为 truespring.cloud.contract.verifier.assert.size。此功能的默认设置是 false。你还可以设置插件配置中的 assertJsonSize 属性。

由于 JSON 结构可能有任意形式,使用 Groovy DSL 和 GString 中的 value(consumer(…​), producer(…​)) 符号时可能无法正确解析它。因此,你应该使用 Groovy Map 符号。

Multiple Contracts in One File

Multiple Contracts in One File

你可以在一个文件中定义多个合同。此类合同可能类似于以下示例:

Groovy
link:{plugins_path}/spring-cloud-contract-maven-plugin/src/test/projects/multiple-contracts/src/test/resources/contracts/com/hello/v1/WithList.groovy[role=include]
YAML
link:{verifier_root_path}/src/test/resources/yml/multiple_contracts.yml[role=include]
Java
class contract implements Supplier<Collection<Contract>> {

	@Override
	public Collection<Contract> get() {
		return Arrays.asList(
            Contract.make(c -> {
            	c.name("should post a user");
                // ...
            }), Contract.make(c -> {
                // ...
            }), Contract.make(c -> {
                // ...
            })
		);
	}

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

arrayOf(
    contract {
        name("should post a user")
        // ...
    },
    contract {
        // ...
    },
    contract {
        // ...
    }
}

在前面的示例中,一个合同具有 name 字段,另一个没有。这会导致生成两个看起来像以下的测试:

package org.springframework.cloud.contract.verifier.tests.com.hello;

import com.example.TestBase;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import com.jayway.restassured.response.ResponseOptions;
import org.junit.Test;

import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;

public class V1Test extends TestBase {

	@Test
	public void validate_should_post_a_user() throws Exception {
		// given:
			MockMvcRequestSpecification request = given();

		// when:
			ResponseOptions response = given().spec(request)
					.post("/users/1");

		// then:
			assertThat(response.statusCode()).isEqualTo(200);
	}

	@Test
	public void validate_withList_1() throws Exception {
		// given:
			MockMvcRequestSpecification request = given();

		// when:
			ResponseOptions response = given().spec(request)
					.post("/users/2");

		// then:
			assertThat(response.statusCode()).isEqualTo(200);
	}

}

请注意,对于具有 name 字段的合同,生成的测试方法被命名为 validate_should_post_a_user。未具有 name 字段的那个被称为 validate_withList_1。它对应于文件 WithList.groovy 的名称和列表中合同的索引。

生成的存根显示在以下示例中:

should post a user.json
1_WithList.json

第一个文件从契约获取了 name 参数。第二个文件使用索引(本例中,契约在文件契约列表中的索引为 1)作为前缀获取契约文件名(WithList.groovy)。

命名合同更好,因为这样做让你的测试更具意义。

Stateful Contracts

Stateful Contracts

有状态合约(也称为场景)是应按顺序读取的合约定义。这在以下情况下可能有用:

  • 因为你使用 Spring Cloud Contract 测试你的有状态应用,所以你想要按一个明确定义的顺序调用合约。

我们非常不建议你这样做,因为合约测试应该是无状态的。

  • 你想要同一端点对于同一个请求返回不同的结果。

要创建有状态合约(或场景),需要在创建合约时使用适当的命名约定。约定要求包含一个后跟下划线的顺序号。无论使用 YAML 还是 Groovy,这都适用。以下列表显示了一个示例:

my_contracts_dir\
  scenario1\
    1_login.groovy
    2_showCart.groovy
    3_logout.groovy

这样的树会导致 Spring Cloud Contract Verifier 生成名称为“scenario1”且存在以下三步的 WireMock 场景:

  1. login,标记为 Started 指向…​

  2. showCart,标记为 Step1 指向…​

  3. logout,标记为 Step2(该标记关闭场景)。

您可以在 https://wiremock.org/docs/stateful-behaviour/ 找到有关 WireMock 场景的更多详细信息。