契約 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 本身也能被程式設計師閱讀。
請記住,在 Groovy 契約檔案中,你需要提供 Contract 類的完全限定名並進行 make 靜態匯入,例如 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
),這樣你就無需修改專案的 classpath。在這種情況下,你需要向 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 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)展示瞭如何操作
<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>"
}
請記住,在 Kotlin Script 檔案中,你需要提供 ContractDSL 類的完全限定名。通常你會使用它的 contract 函式,如下所示:org.springframework.cloud.contract.spec.ContractDsl.contract { … } 。你也可以匯入 contract 函式(import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract ),然後呼叫 contract { … } 。 |
YAML 中的契約 DSL
要檢視 YAML 契約的 schema,請訪問 YML Schema 頁面。
限制
對驗證 JSON 陣列大小的支援尚處於實驗階段。如果你想啟用它,請將以下系統屬性的值設定為 true :spring.cloud.contract.verifier.assert.size 。預設情況下,此功能設定為 false 。你也可以在外掛配置中設定 assertJsonSize 屬性。 |
由於 JSON 結構可以是任意形式,因此在使用 Groovy DSL 和 GString 中的 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 場景,包含以下三個步驟
-
login
,標記為Started
,指向… -
showCart
,標記為Step1
,指向… -
logout
,標記為Step2
(這將結束場景)。
你可以在 https://wiremock.org/docs/stateful-behaviour/ 找到關於 WireMock 場景的更多詳細資訊。