Contract DSL
Spring Cloud Contract 支援以以下語言編寫的 DSL
-
Groovy
-
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());
});
});
}));
}
}
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 命令將 contract 編譯為 stub 對應 mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert |
Groovy 中的 Contract DSL
如果您不熟悉 Groovy,請別擔心。您也可以在 Groovy DSL 檔案中使用 Java 語法。
如果您決定以 Groovy 編寫 contract,如果您之前沒有使用過 Groovy,請不要驚慌。實際上並不需要了解該語言,因為 Contract DSL 僅使用其一小部分(僅限字面值、方法呼叫和閉包)。此外,DSL 是靜態類型的,使其對程式設計人員來說是可讀的,而無需任何 DSL 本身的知識。
請記住,在 Groovy contract 檔案中,您必須為 Contract 類別提供完整限定名稱,並進行 make 靜態匯入,例如 org.springframework.cloud.spec.Contract.make { … } 。您也可以為 Contract 類別提供匯入 (import org.springframework.cloud.spec.Contract ),然後呼叫 Contract.make { … } 。 |
Java 中的 Contract DSL
若要以 Java 編寫 contract 定義,您需要建立一個類別,該類別實作 Supplier<Contract>
介面(適用於單一 contract)或 Supplier<Collection<Contract>>
(適用於多個 contract)。
您也可以在 src/test/java
下編寫 contract 定義(例如,src/test/java/contracts
),這樣您就不必修改專案的類別路徑。在這種情況下,您必須為 Spring Cloud Contract 外掛程式提供 contract 定義的新位置。
以下範例(Maven 和 Gradle 皆有)在 src/test/java
下具有 contract 定義
<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 中的 Contract DSL
若要開始以 Kotlin 編寫 contract,您需要從(新建立的)Kotlin Script 檔案 (.kts
) 開始。與 Java DSL 一樣,您可以將 contract 放在您選擇的任何目錄中。依預設,Maven 外掛程式將查看 src/test/resources/contracts
目錄,而 Gradle 外掛程式將查看 src/contractTest/resources/contracts
目錄。
自 3.0.0 起,Gradle 外掛程式也會為了移轉目的而查看舊版目錄 src/test/resources/contracts 。當在此目錄中找到 contract 時,您的建置期間將會記錄警告。 |
您需要明確地將 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 中的 Contract DSL
若要查看 YAML contract 的結構描述,請造訪 YML Schema 頁面。
限制
對 JSON 陣列大小進行驗證的支援是實驗性的。如果您想要開啟它,請將以下系統屬性的值設定為 true :spring.cloud.contract.verifier.assert.size 。依預設,此功能設定為 false 。您也可以在外掛程式組態中設定 assertJsonSize 屬性。 |
由於 JSON 結構可以有任何形式,因此在使用 Groovy DSL 和 value(consumer(…), producer(…)) 表示法在 GString 中時,可能無法正確剖析它。這就是為什麼您應該使用 Groovy Map 表示法的原因。 |
單一檔案中的多個 Contract
您可以在一個檔案中定義多個 contract。這樣的 contract 可能類似於以下範例
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 {
// ...
}
}
在先前的範例中,一個 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
欄位的 contract,產生的測試方法命名為 validate_should_post_a_user
。沒有 name
欄位的 contract 稱為 validate_withList_1
。它對應於檔案 WithList.groovy
的名稱以及 contract 在列表中的索引。
產生的 stub 顯示在以下範例中
should post a user.json
1_WithList.json
第一個檔案從 contract 取得 name
參數。第二個檔案取得 contract 檔案的名稱 (WithList.groovy
),並以索引作為前綴(在此案例中,contract 在檔案中的 contract 列表中具有索引 1
)。
最好命名您的 contract,因為這樣做會使您的測試更有意義。 |
具狀態的 Contract
具狀態的 contract(也稱為情境)是應該依序讀取的 contract 定義。這在以下情況下可能很有用
-
您想要以精確定義的順序調用 contract,因為您使用 Spring Cloud Contract 來測試您的具狀態應用程式。
我們真的不鼓勵您這樣做,因為 contract 測試應該是無狀態的。 |
-
您希望相同的端點針對相同的要求傳回不同的結果。
若要建立具狀態的 contract(或情境),您需要在建立 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 情境的更多詳細資訊。