Contract DSL

Spring Cloud Contract 支援以以下語言編寫的 DSL

  • Groovy

  • YAML

  • Java

  • Kotlin

Spring Cloud Contract 支援在單一檔案中定義多個 contract(在 Groovy 中,傳回列表而不是單一 contract)。

以下範例顯示 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()
	}
}
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 命令將 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 定義

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

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 皆有)示範如何執行此操作

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>"
}
請記住,在 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 陣列大小進行驗證的支援是實驗性的。如果您想要開啟它,請將以下系統屬性的值設定為 truespring.cloud.contract.verifier.assert.size。依預設,此功能設定為 false。您也可以在外掛程式組態中設定 assertJsonSize 屬性。
由於 JSON 結構可以有任何形式,因此在使用 Groovy DSL 和 value(consumer(…​), producer(…​)) 表示法在 GString 中時,可能無法正確剖析它。這就是為什麼您應該使用 Groovy Map 表示法的原因。

單一檔案中的多個 Contract

您可以在一個檔案中定義多個 contract。這樣的 contract 可能類似於以下範例

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 {
        // ...
    }
}

在先前的範例中,一個 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 情境和以下三個步驟

  1. login,標記為 Started,指向…​

  2. showCart,標記為 Step1,指向…​

  3. logout,標記為 Step2(關閉情境)。

您可以在 https://wiremock.org/docs/stateful-behaviour/ 找到有關 WireMock 情境的更多詳細資訊。