跳到主要内容

Contract DSL

DeepSeek V3 中英对照 Contract DSL

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

  • Groovy

  • YAML

  • Java

  • Kotlin

提示

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

以下示例展示了一个合约定义:

org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/api/12'
headers {
header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json'
}
body '''\
[{
"created_at": "Sat Jul 26 09:38:57 +0000 2014",
"id": 492967299297845248,
"id_str": "492967299297845248",
"text": "Gonna see you at Warsaw",
"place":
{
"attributes":{},
"bounding_box":
{
"coordinates":
[[
[-77.119759,38.791645],
[-76.909393,38.791645],
[-76.909393,38.995548],
[-77.119759,38.995548]
]],
"type":"Polygon"
},
"country":"United States",
"country_code":"US",
"full_name":"Washington, DC",
"id":"01fbe706f872cb32",
"name":"Washington",
"place_type":"city",
"url": "https://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
}
}]
'''
}
response {
status OK()
}
}
description: Some description
name: some name
priority: 8
ignored: true
request:
url: /foo
queryParameters:
a: b
b: c
method: PUT
headers:
foo: bar
fooReq: baz
body:
foo: bar
matchers:
body:
- path: $.foo
type: by_regex
value: bar
headers:
- key: foo
regex: bar
response:
status: 200
headers:
foo2: bar
foo3: foo33
fooRes: baz
body:
foo2: bar
foo3: baz
nullValue: null
matchers:
body:
- path: $.foo2
type: by_regex
value: bar
- path: $.foo3
type: by_command
value: executeMe($it)
- path: $.nullValue
type: by_null
value: null
headers:
- key: foo2
regex: bar
- key: foo3
command: andMeToo($it)
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;

class contract_rest implements Supplier<Collection<Contract>> {

@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.description("Some description");
c.name("some name");
c.priority(8);
c.ignored();
c.request(r -> {
r.url("/foo", u -> {
u.queryParameters(q -> {
q.parameter("a", "b");
q.parameter("b", "c");
});
});
r.method(r.PUT());
r.headers(h -> {
h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar")));
h.header("fooReq", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo", "bar"));
r.bodyMatchers(m -> {
m.jsonPath("$.foo", m.byRegex("bar"));
});
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
r.status(r.OK());
r.headers(h -> {
h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar")));
h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")), r.client("foo33")));
h.header("fooRes", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo2", "bar").entry("foo3", "baz").entry("nullValue", null));
r.bodyMatchers(m -> {
m.jsonPath("$.foo2", m.byRegex("bar"));
m.jsonPath("$.foo3", m.byCommand("executeMe($it)"));
m.jsonPath("$.nullValue", m.byNull());
});
});
}));
}

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

contract {
name = "some name"
description = "Some description"
priority = 8
ignored = true
request {
url = url("/foo") withQueryParameters {
parameter("a", "b")
parameter("b", "c")
}
method = PUT
headers {
header("foo", value(client(regex("bar")), server("bar")))
header("fooReq", "baz")
}
body = body(mapOf("foo" to "bar"))
bodyMatchers {
jsonPath("$.foo", byRegex("bar"))
}
}
response {
delay = fixedMilliseconds(1000)
status = OK
headers {
header("foo2", value(server(regex("bar")), client("bar")))
header("foo3", value(server(execute("andMeToo(\$it)")), client("foo33")))
header("fooRes", "baz")
}
body = body(mapOf(
"foo" to "bar",
"foo3" to "baz",
"nullValue" to null
))
bodyMatchers {
jsonPath("$.foo2", byRegex("bar"))
jsonPath("$.foo3", byCommand("executeMe(\$it)"))
jsonPath("$.nullValue", byNull)
}
}
}
提示

你可以通过以下独立的 Maven 命令将合约编译为存根映射:

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

Groovy 中的合约 DSL​

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

如果你决定使用 Groovy 编写合约,即使你之前从未使用过 Groovy 也不必担心。实际上并不需要掌握这门语言的知识,因为合约 DSL 仅使用了其极小的子集(仅包括字面量、方法调用和闭包)。此外,该 DSL 是静态类型的,这使得即使不了解 DSL 本身,程序员也能轻松阅读。

important

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

Java 中的合约 DSL​

要在Java中编写合约定义,你需要创建一个实现Supplier<Contract>接口(用于单个合约)或Supplier<Collection<Contract>>接口(用于多个合约)的类。

您也可以将合约定义写在 src/test/java 目录下(例如 src/test/java/contracts),这样就不必修改项目的类路径。在这种情况下,您需要向 Spring Cloud Contract 插件提供合约定义的新位置。

以下示例(包括 Maven 和 Gradle)将合约定义放在 src/test/java 目录下:

<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>
contracts {
contractsDslDir = new File(project.rootDir, "src/test/java/contracts")
}

Kotlin 中的合约 DSL​

要开始使用 Kotlin 编写合约,你需要从一个(新创建的)Kotlin 脚本文件(.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)展示了如何操作:

<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>
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>"
}
important

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

合约DSL的YAML格式​

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

局限性​

注意

对验证 JSON 数组大小的支持目前处于实验阶段。如果您想启用此功能,请将以下系统属性的值设置为 true:spring.cloud.contract.verifier.assert.size。默认情况下,此功能设置为 false。您也可以在插件配置中设置 assertJsonSize 属性。

注意

由于 JSON 结构可以具有任意形式,当在 GString 中使用 Groovy DSL 和 value(consumer(…​), producer(…​)) 表示法时,可能无法正确解析它。这就是为什么你应该使用 Groovy Map 表示法。

单个文件中的多个合约​

你可以在一个文件中定义多个合约。这样的合约可能类似于以下示例:

import org.springframework.cloud.contract.spec.Contract

[
Contract.make {
name("should post a user")
request {
method 'POST'
url('/users/1')
}
response {
status OK()
}
},
Contract.make {
request {
method 'POST'
url('/users/2')
}
response {
status OK()
}
}
]
---
name: should post a user
request:
method: POST
url: /users/1
response:
status: 200
---
request:
method: POST
url: /users/2
response:
status: 200
---
request:
method: POST
url: /users/3
response:
status: 200
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 -> {
// ...
})
);
}

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

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

在前面的例子中,一个合约有 name 字段,而另一个没有。这导致生成了两个看起来如下的测试:

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 参数。第二个文件则获取了合约文件的名称(WithList.groovy),并在前面加上了索引(在本例中,该合约在文件中的合约列表索引为 1)。

提示

为你的合约命名会好得多,因为这样做能让你的测试更有意义。

有状态合约​

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

  • 您希望以明确定义的顺序调用合约,因为您使用 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 场景的更多详细信息。

章节总结​