此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 spring-cloud-contract 4.2.0! |
合约 DSL
Spring Cloud Contract 支持以下语言编写的 DSL:
-
槽的
-
YAML
-
Java
-
Kotlin
Spring Cloud Contract 支持在单个文件中定义多个 Contract (在 Groovy 中返回一个列表而不是单个 Contract )。 |
以下示例显示了 Contract 定义:
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());
});
});
}));
}
}
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)
}
}
}
You can compile contracts to stubs mapping by using the following standalone Maven command:
mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert
Contract DSL in Groovy
If you are not familiar with Groovy, do not worry. You can use Java syntax in the
Groovy DSL files as well.
If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy
before. Knowledge of the language is not really needed, as the Contract DSL uses only a
tiny subset of it (only literals, method calls, and closures). Also, the DSL is statically
typed, to make it programmer-readable without any knowledge of the DSL itself.
Remember that, inside the Groovy contract file, you have to provide the fully
qualified name to the Contract
class and make
static imports, such as
org.springframework.cloud.spec.Contract.make { … }
. You can also provide an import to
the Contract
class (import org.springframework.cloud.spec.Contract
) and then call
Contract.make { … }
.
Contract DSL in Java
To write a contract definition in Java, you need to create a class that implements either the Supplier<Contract>
interface (for a single contract) or Supplier<Collection<Contract>>
(for multiple contracts).
You can also write the contract definitions under src/test/java
(for example, src/test/java/contracts
) so that you do not have to modify the classpath of your project. In this case, you have to provide a new location of contract definitions to your Spring Cloud Contract plugin.
The following example (in both Maven and Gradle) has the contract definitions under 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
To get started with writing contracts in Kotlin, you need to start with a (newly created) Kotlin Script file (.kts
).
As with the Java DSL, you can put your contracts in any directory of your choice.
By default, the Maven plugin will look at the src/test/resources/contracts
directory and Gradle plugin will
look at the src/contractTest/resources/contracts
directory.
Since 3.0.0, the Gradle plugin will also look at the legacy
directory src/test/resources/contracts
for migration purposes. When contracts are found in this directory, a warning
will be logged during your build.
You need to explicitly pass the spring-cloud-contract-spec-kotlin
dependency to your project plugin setup.
The following example (in both Maven and Gradle) shows how to do so:
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>"
}
Remember that, inside the Kotlin Script file, you have to provide the fully qualified name to the ContractDSL
class.
Generally you would use its contract function as follows: org.springframework.cloud.contract.spec.ContractDsl.contract { … }
.
You can also provide an import to the contract
function (import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
) and then call contract { … }
.
Contract DSL in YAML
To see a schema of a YAML contract, visit the YML Schema page.
Limitations
The support for verifying the size of JSON arrays is experimental. If you want
to turn it on, set the value of the following system property to true
:
spring.cloud.contract.verifier.assert.size
. By default, this feature is set to false
.
You can also set the assertJsonSize
property in the plugin configuration.
Because JSON structure can have any form, it can be impossible to parse it
properly when using the Groovy DSL and the value(consumer(…), producer(…))
notation in GString
. That
is why you should use the Groovy Map notation.
Multiple Contracts in One File
You can define multiple contracts in one file. Such a contract might resemble the
following example:
Groovy
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 {
// ...
}
}
In the preceding example, one contract has the name
field and the other does not. This
leads to generation of two tests that look like the following:
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);
}
}
Notice that, for the contract that has the name
field, the generated test method is named
validate_should_post_a_user
. The one that does not have the name
field is called
validate_withList_1
. It corresponds to the name of the file WithList.groovy
and the
index of the contract in the list.
The generated stubs are shown in the following example:
should post a user.json
1_WithList.json
The first file got the name
parameter from the contract. The second
got the name of the contract file (WithList.groovy
) prefixed with the index (in this
case, the contract had an index of 1
in the list of contracts in the file).
It is much better to name your contracts, because doing so makes
your tests far more meaningful.
Stateful Contracts
Stateful contracts (also known as scenarios) are contract definitions that should be read
in order. This might be useful in the following situations:
-
You want to invoke the contract in a precisely defined order, since you use Spring
Cloud Contract to test your stateful application.
We really discourage you from doing that, since contract tests should be stateless.
-
You want the same endpoint to return different results for the same request.
To create stateful contracts (or scenarios), you need to
use the proper naming convention while creating your contracts. The convention
requires including an order number followed by an underscore. This works regardless
of whether you work with YAML or Groovy. The following listing shows an example:
my_contracts_dir\
scenario1\
1_login.groovy
2_showCart.groovy
3_logout.groovy
Such a tree causes Spring Cloud Contract Verifier to generate WireMock’s scenario with a
name of scenario1
and the three following steps:
-
login
, marked as Started
pointing to…
-
showCart
, marked as Step1
pointing to…
-
logout
, marked as Step2
(which closes the scenario).
You can find more details about WireMock scenarios at
https://wiremock.org/docs/stateful-behaviour/.