契約 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 を使用したことがなくても心配する必要はありません。Contract 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
) に契約定義を記述することもできます。この場合、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 スクリプトファイル (.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 スクリプトファイル内で、ContractDSL クラスに完全修飾名を指定する必要があることに注意してください。一般に、その契約関数は org.springframework.cloud.contract.spec.ContractDsl.contract { … } のように使用します。contract 関数 (import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract ) にインポートを提供してから、contract { … } を呼び出すこともできます。 |
YAML での契約 DSL
YAML 契約のスキーマを確認するには、YML スキーマページにアクセスしてください。
制限
JSON 配列のサイズを検証するためのサポートは実験的なものです。これをオンにしたい場合は、次のシステムプロパティの値を true : spring.cloud.contract.verifier.assert.size に設定します。デフォルトでは、この機能は false に設定されています。プラグイン設定で assertJsonSize プロパティを設定することもできます。 |
JSON 構造は任意の形式をとることができるため、Groovy DSL および GString の value(consumer(…), producer(…)) 表記を使用すると、JSON 構造を適切に解析できない可能性があります。このため、Groovy マップ表記を使用する必要があります。 |
複数の契約を 1 つのファイルに
1 つのファイルで複数の契約を定義できます。このような契約は次の例のようになります。
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 {
// ...
}
}
前述の例では、1 つの契約には name
フィールドがあり、もう 1 つはありません。これにより、次のような 2 つのテストが生成されます。
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
パラメーターを取得しました。2 つ目は、インデックスが接頭辞として付けられた契約ファイル名 (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
という名前と次の 3 つのステップを持つ WireMock のシナリオを生成します。
login
、Started
としてマークされており、次のことを指します。…showCart
、Step1
としてマークされており、次のことを指します。…logout
、Step2
としてマークされます (これでシナリオが終了します)。
WireMock シナリオの詳細については、https://wiremock.org/docs/stateful-behaviour/ (英語) を参照してください。