对于最新的稳定版本,请使用 spring-cloud-contract 4.3.0spring-doc.cadn.net.cn

合约 DSL

Spring Cloud Contract 支持用以下语言编写的 DSL:spring-doc.cadn.net.cn

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

以下示例显示了合同定义:spring-doc.cadn.net.cn

槽的
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()
	}
}
YAML
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)
Java
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());
				});
			});
		}));
	}

}
Kotlin
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 命令编译合约到存根映射:spring-doc.cadn.net.cn

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

Groovy 中的合约 DSL

如果您不熟悉 Groovy,请不要担心。您也可以在Groovy DSL 文件中使用 Java 语法。spring-doc.cadn.net.cn

如果您决定在 Groovy 中编写合同,如果您还没有使用 Groovy,请不要惊慌 以前。 实际上并不需要该语言的知识,因为合约 DSL 只使用它的一小部分(仅文字、方法调用和闭包)。此外,DSL 是静态的类型化的,以使其程序员无需了解 DSL 本身即可阅读。spring-doc.cadn.net.cn

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

Java 中的合约 DSL

要用 Java 编写合约定义,您需要创建一个类来实现Supplier<Contract>接口(对于单个合约)或Supplier<Collection<Contract>>(对于多个合同)。spring-doc.cadn.net.cn

您还可以在src/test/java(例如,src/test/java/contracts),这样您就不必修改项目的类路径。在这种情况下,您必须向 Spring Cloud Contract 插件提供契约定义的新位置。spring-doc.cadn.net.cn

以下示例(在 Maven 和 Gradle 中)在src/test/java:spring-doc.cadn.net.cn

专家
<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")
}

Kotlin 中的合约 DSL

要开始在 Kotlin 中编写合约,您需要从(新创建的)Kotlin 脚本文件 (.kts). 与 Java DSL 一样,您可以将合约放在您选择的任何目录中。默认情况下,Maven 插件将查看src/test/resources/contracts目录和 Gradle 插件将查看src/contractTest/resources/contracts目录。spring-doc.cadn.net.cn

从 3.0.0 开始,Gradle 插件也将查看旧版 目录src/test/resources/contracts用于迁移目的。在此目录中找到合约时,将在构建过程中记录警告。

您需要显式传递spring-cloud-contract-spec-kotlin依赖于您的项目插件设置。以下示例(在 Maven 和 Gradle 中)展示了如何执行此作:spring-doc.cadn.net.cn

专家
<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类。 通常,您将按如下方式使用其合约函数:org.springframework.cloud.contract.spec.ContractDsl.contract { …​ }. 您还可以将导入提供到contract函数 (import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract),然后调用contract { …​ }.

YAML 中的合约 DSL

要查看 YAML 合约的架构,请访问 YML 架构页面。spring-doc.cadn.net.cn

局限性

对验证 JSON 数组大小的支持是实验性的。 如果需要帮助, 要打开它,请将以下系统属性的值设置为true:spring.cloud.contract.verifier.assert.size. 默认情况下,此功能设置为false. 您还可以将assertJsonSize插件配置中的属性。
因为 JSON 结构可以有任何形式,所以不可能解析它正确使用 Groovy DSL 和value(consumer(…​), producer(…​))符号GString. 那 这就是为什么您应该使用 Groovy Map 表示法。

一个文件中的多个合同

您可以在一个文件中定义多个合同。这样的合同可能类似于以下示例:spring-doc.cadn.net.cn

槽的
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()
		}
	}
]
YAML
---
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
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字段,另一个则没有。 这 导致生成如下所示的两个测试:spring-doc.cadn.net.cn

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和 列表中合约的索引。spring-doc.cadn.net.cn

生成的存根如以下示例所示:spring-doc.cadn.net.cn

should post a user.json
1_WithList.json

第一个文件获得了name参数。第二个 获取合约文件的名称 (WithList.groovy) 以索引为前缀(在此 案例,则合约的索引为1在文件中的合同列表中)。spring-doc.cadn.net.cn

最好为您的合同命名,因为这样做会使 您的测试更有意义。

有状态合约

有状态协定(也称为方案)是应读取的协定定义 挨次。这在以下情况下可能很有用:spring-doc.cadn.net.cn

  • 您希望以精确定义的顺序调用合约,因为您使用 Spring 云合约来测试有状态应用程序。spring-doc.cadn.net.cn

我们真的不鼓励你这样做,因为合约测试应该是无状态的。

若要创建有状态合约(或方案),需要 在创建合同时使用正确的命名约定。大会 需要包括订单号,后跟下划线。无论如何,这都有效 无论您是使用 YAML 还是 Groovy。以下列表显示了一个示例:spring-doc.cadn.net.cn

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

这样的树导致 Spring Cloud Contract Verifier 生成 WireMock 的场景,其中包含 名称scenario1以及以下三个步骤:spring-doc.cadn.net.cn

  1. login,标记为Started指向...spring-doc.cadn.net.cn

  2. showCart,标记为Step1指向...spring-doc.cadn.net.cn

  3. logout,标记为Step2(这结束了场景)。spring-doc.cadn.net.cn

可以在 https://wiremock.org/docs/stateful-behaviour/ 找到有关 WireMock 方案的更多详细信息。spring-doc.cadn.net.cn