動的プロパティ
契約には、タイムスタンプ、ID などのいくつかの動的プロパティを含めることができます。スタブによって一致するように、常に同じ時刻値を返すようにコンシューマーにクロックをスタブ化させることは望ましくありません。
Groovy DSL の場合、契約内の動的部分を 2 つの方法で提供できます。本体に直接渡すか、bodyMatchers
という別のセクションに設定します。
2.0.0 より前は、これらは testMatchers および stubMatchers を使用して設定されていました。詳細については、移行ガイド [GitHub] (英語) を参照してください。 |
YAML の場合、matchers
セクションのみを使用できます。
matchers 内のエントリは、ペイロードの既存の要素を参照する必要があります。詳細については、この課題 [GitHub] (英語) を参照してください。 |
ボディ内部の動的特性
このセクションは、コード化 DSL (Groovy、Java など) に対してのみ有効です。同様の機能の YAML の例については、"Matchers セクションの動的プロパティ" セクションを参照してください。 |
本体内のプロパティは、value
メソッドを使用して設定するか、Groovy マップ表記法を使用する場合は $()
を使用して設定できます。次の例は、value メソッドを使用して動的プロパティを設定する方法を示しています。
value(consumer(...), producer(...))
value(c(...), p(...))
value(stub(...), test(...))
value(client(...), server(...))
$(consumer(...), producer(...))
$(c(...), p(...))
$(stub(...), test(...))
$(client(...), server(...))
どちらのアプローチも同様に機能します。stub
メソッドと client
メソッドは、consumer
メソッドのエイリアスです。後続のセクションでは、これらの値を使用して何ができるかを詳しく見ていきます。
正規表現
このセクションは、Groovy DSL に対してのみ有効です。同様の機能の YAML の例については、"Matchers セクションの動的プロパティ" セクションを参照してください。 |
正規表現を使用して、契約 DSL にリクエストを記述することができます。これは、特定のパターンに従うリクエストに対して特定のレスポンスを提供する必要があることを示したい場合に特に便利です。また、テストとサーバー側テストの両方で正確な値ではなくパターンを使用する必要がある場合は、正規表現を使用できます。
内部的には Pattern.matches()
(標準 Javadoc) が呼び出されるため、正規表現がシーケンスの領域全体に一致することを確認してください。たとえば、abc
は aabc
と一致しませんが、.abc
は一致します。さらにいくつかの既知の制限もあります。
次の例は、正規表現を使用してリクエストを作成する方法を示しています。
org.springframework.cloud.contract.spec.Contract.make {
request {
method('GET')
url $(consumer(~/\/[0-9]{2}/), producer('/12'))
}
response {
status OK()
body(
id: $(anyNumber()),
surname: $(
consumer('Kowalsky'),
producer(regex('[a-zA-Z]+'))
),
name: 'Jan',
created: $(consumer('2014-02-02 12:23:43'), producer(execute('currentDate(it)'))),
correlationId: value(consumer('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
producer(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
)
)
headers {
header 'Content-Type': 'text/plain'
}
}
}
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.request(r -> {
r.method("GET");
r.url(r.$(r.consumer(r.regex("\\/[0-9]{2}")), r.producer("/12")));
});
c.response(r -> {
r.status(r.OK());
r.body(ContractVerifierUtil.map()
.entry("id", r.$(r.anyNumber()))
.entry("surname", r.$(r.consumer("Kowalsky"), r.producer(r.regex("[a-zA-Z]+")))));
r.headers(h -> {
h.header("Content-Type", "text/plain");
});
});
});
contract {
request {
method = method("GET")
url = url(v(consumer(regex("\\/[0-9]{2}")), producer("/12")))
}
response {
status = OK
body(mapOf(
"id" to v(anyNumber),
"surname" to v(consumer("Kowalsky"), producer(regex("[a-zA-Z]+")))
))
headers {
header("Content-Type", "text/plain")
}
}
}
正規表現を使用して通信の片側のみを提供することもできます。これを行うと、契約 エンジンは、指定された正規表現に一致する生成された文字列を自動的に提供します。次のコードは、Groovy の例を示しています。
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url value(consumer(regex('/foo/[0-9]{5}')))
body([
requestElement: $(consumer(regex('[0-9]{5}')))
])
headers {
header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*'))))
}
}
response {
status OK()
body([
responseElement: $(producer(regex('[0-9]{7}')))
])
headers {
contentType("application/vnd.fraud.v1+json")
}
}
}
前述の例では、通信の反対側で、リクエストとレスポンス用にそれぞれ生成されたデータが送信されます。
Spring Cloud Contract には、次の例に示すように、契約で使用できる一連の事前定義された正規表現が付属しています。
public static RegexProperty onlyAlphaUnicode() {
return new RegexProperty(ONLY_ALPHA_UNICODE).asString();
}
public static RegexProperty alphaNumeric() {
return new RegexProperty(ALPHA_NUMERIC).asString();
}
public static RegexProperty number() {
return new RegexProperty(NUMBER).asDouble();
}
public static RegexProperty positiveInt() {
return new RegexProperty(POSITIVE_INT).asInteger();
}
public static RegexProperty anyBoolean() {
return new RegexProperty(TRUE_OR_FALSE).asBooleanType();
}
public static RegexProperty anInteger() {
return new RegexProperty(INTEGER).asInteger();
}
public static RegexProperty aDouble() {
return new RegexProperty(DOUBLE).asDouble();
}
public static RegexProperty ipAddress() {
return new RegexProperty(IP_ADDRESS).asString();
}
public static RegexProperty hostname() {
return new RegexProperty(HOSTNAME_PATTERN).asString();
}
public static RegexProperty email() {
return new RegexProperty(EMAIL).asString();
}
public static RegexProperty url() {
return new RegexProperty(URL).asString();
}
public static RegexProperty httpsUrl() {
return new RegexProperty(HTTPS_URL).asString();
}
public static RegexProperty uuid() {
return new RegexProperty(UUID).asString();
}
public static RegexProperty uuid4() {
return new RegexProperty(UUID4).asString();
}
public static RegexProperty isoDate() {
return new RegexProperty(ANY_DATE).asString();
}
public static RegexProperty isoDateTime() {
return new RegexProperty(ANY_DATE_TIME).asString();
}
public static RegexProperty isoTime() {
return new RegexProperty(ANY_TIME).asString();
}
public static RegexProperty iso8601WithOffset() {
return new RegexProperty(ISO8601_WITH_OFFSET).asString();
}
public static RegexProperty nonEmpty() {
return new RegexProperty(NON_EMPTY).asString();
}
public static RegexProperty nonBlank() {
return new RegexProperty(NON_BLANK).asString();
}
契約では、次のように使用できます (Groovy DSL の例)。
Contract dslWithOptionalsInString = Contract.make {
priority 1
request {
method POST()
url '/users/password'
headers {
contentType(applicationJson())
}
body(
email: $(consumer(optional(regex(email()))), producer('[email protected] (英語) ')),
callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
)
}
response {
status 404
headers {
contentType(applicationJson())
}
body(
code: value(consumer("123123"), producer(optional("123123"))),
message: "User not found by email = [${value(producer(regex(email())), consumer('[email protected] (英語) '))}]"
)
}
}
問題をさらに単純にするために、正規表現が渡されることを自動的に想定する事前定義されたオブジェクトのセットを使用できます。これらのメソッドはすべて、次のように any
プレフィックスで始まります。
T anyAlphaUnicode();
T anyAlphaNumeric();
T anyNumber();
T anyInteger();
T anyPositiveInt();
T anyDouble();
T anyHex();
T aBoolean();
T anyIpAddress();
T anyHostname();
T anyEmail();
T anyUrl();
T anyHttpsUrl();
T anyUuid();
T anyDate();
T anyDateTime();
T anyTime();
T anyIso8601WithOffset();
T anyNonBlankString();
T anyNonEmptyString();
T anyOf(String... values);
次の例は、これらのメソッドを参照する方法を示しています。
Contract contractDsl = Contract.make {
name "foo"
label 'trigger_event'
input {
triggeredBy('toString()')
}
outputMessage {
sentTo 'topic.rateablequote'
body([
alpha : $(anyAlphaUnicode()),
number : $(anyNumber()),
anInteger : $(anyInteger()),
positiveInt : $(anyPositiveInt()),
aDouble : $(anyDouble()),
aBoolean : $(aBoolean()),
ip : $(anyIpAddress()),
hostname : $(anyHostname()),
email : $(anyEmail()),
url : $(anyUrl()),
httpsUrl : $(anyHttpsUrl()),
uuid : $(anyUuid()),
date : $(anyDate()),
dateTime : $(anyDateTime()),
time : $(anyTime()),
iso8601WithOffset: $(anyIso8601WithOffset()),
nonBlankString : $(anyNonBlankString()),
nonEmptyString : $(anyNonEmptyString()),
anyOf : $(anyOf('foo', 'bar'))
])
}
}
contract {
name = "foo"
label = "trigger_event"
input {
triggeredBy = "toString()"
}
outputMessage {
sentTo = sentTo("topic.rateablequote")
body(mapOf(
"alpha" to v(anyAlphaUnicode),
"number" to v(anyNumber),
"anInteger" to v(anyInteger),
"positiveInt" to v(anyPositiveInt),
"aDouble" to v(anyDouble),
"aBoolean" to v(aBoolean),
"ip" to v(anyIpAddress),
"hostname" to v(anyAlphaUnicode),
"email" to v(anyEmail),
"url" to v(anyUrl),
"httpsUrl" to v(anyHttpsUrl),
"uuid" to v(anyUuid),
"date" to v(anyDate),
"dateTime" to v(anyDateTime),
"time" to v(anyTime),
"iso8601WithOffset" to v(anyIso8601WithOffset),
"nonBlankString" to v(anyNonBlankString),
"nonEmptyString" to v(anyNonEmptyString),
"anyOf" to v(anyOf('foo', 'bar'))
))
headers {
header("Content-Type", "text/plain")
}
}
}
制限
正規表現から文字列を生成する Xeger ライブラリには特定の制限があるため、自動生成に依存する場合は、正規表現で $ および ^ 記号を使用しないでください。899 号 [GitHub] (英語) を参照してください。 |
LocalDate インスタンスを $ の値として使用しないでください (たとえば、$(consumer(LocalDate.now())) )。java.lang.StackOverflowError が発生します。代わりに $(consumer(LocalDate.now().toString())) を使用してください。900 号 [GitHub] (英語) を参照してください。 |
オプションのパラメーターを渡す
このセクションは、Groovy DSL に対してのみ有効です。同様の機能の YAML の例については、"Matchers セクションの動的プロパティ" セクションを参照してください。 |
契約でオプションのパラメーターを指定できます。ただし、オプションのパラメーターは次の場合にのみ指定できます。
リクエストの STUB 側
レスポンスの TEST 側
次の例は、オプションのパラメーターを指定する方法を示しています。
org.springframework.cloud.contract.spec.Contract.make {
priority 1
name "optionals"
request {
method 'POST'
url '/users/password'
headers {
contentType(applicationJson())
}
body(
email: $(consumer(optional(regex(email()))), producer('[email protected] (英語) ')),
callback_url: $(consumer(regex(hostname())), producer('https://partners.com'))
)
}
response {
status 404
headers {
header 'Content-Type': 'application/json'
}
body(
code: value(consumer("123123"), producer(optional("123123")))
)
}
}
org.springframework.cloud.contract.spec.Contract.make(c -> {
c.priority(1);
c.name("optionals");
c.request(r -> {
r.method("POST");
r.url("/users/password");
r.headers(h -> {
h.contentType(h.applicationJson());
});
r.body(ContractVerifierUtil.map()
.entry("email", r.$(r.consumer(r.optional(r.regex(r.email()))), r.producer("[email protected] (英語) ")))
.entry("callback_url",
r.$(r.consumer(r.regex(r.hostname())), r.producer("https://partners.com"))));
});
c.response(r -> {
r.status(404);
r.headers(h -> {
h.header("Content-Type", "application/json");
});
r.body(ContractVerifierUtil.map()
.entry("code", r.value(r.consumer("123123"), r.producer(r.optional("123123")))));
});
});
contract { c ->
priority = 1
name = "optionals"
request {
method = POST
url = url("/users/password")
headers {
contentType = APPLICATION_JSON
}
body = body(mapOf(
"email" to v(consumer(optional(regex(email))), producer("[email protected] (英語) ")),
"callback_url" to v(consumer(regex(hostname)), producer("https://partners.com"))
))
}
response {
status = NOT_FOUND
headers {
header("Content-Type", "application/json")
}
body(mapOf(
"code" to value(consumer("123123"), producer(optional("123123")))
))
}
}
optional()
メソッドで本文の一部をラップすることにより、0 回以上存在する必要がある正規表現を作成します。
Spock を使用する場合、前の例から次のテストが生成されます。
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification
import io.restassured.response.ResponseOptions
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*
class FooSpec extends Specification {
\tdef validate_optionals() throws Exception {
\t\tgiven:
\t\t\tMockMvcRequestSpecification request = given()
\t\t\t\t\t.header("Content-Type", "application/json")
\t\t\t\t\t.body('''{"email":"[email protected] (英語) ","callback_url":"https://partners.com"}''')
\t\twhen:
\t\t\tResponseOptions response = given().spec(request)
\t\t\t\t\t.post("/users/password")
\t\tthen:
\t\t\tresponse.statusCode() == 404
\t\t\tresponse.header("Content-Type") == 'application/json'
\t\tand:
\t\t\tDocumentContext parsedJson = JsonPath.parse(response.body.asString())
\t\t\tassertThatJson(parsedJson).field("['code']").matches("(123123)?")
\t}
}
次のスタブも生成されます。
'''
{
"request" : {
"url" : "/users/password",
"method" : "POST",
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['email'] =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6})?/)]"
}, {
"matchesJsonPath" : "$[?(@.['callback_url'] =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]"
} ],
"headers" : {
"Content-Type" : {
"equalTo" : "application/json"
}
}
},
"response" : {
"status" : 404,
"body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [[email protected] (英語) ]\\"}",
"headers" : {
"Content-Type" : "application/json"
}
},
"priority" : 1
}
'''
サーバー側でのカスタムメソッドの呼び出し
このセクションは、Groovy DSL に対してのみ有効です。同様の機能の YAML の例については、"Matchers セクションの動的プロパティ" セクションを参照してください。 |
テスト中にサーバー側で実行されるメソッド呼び出しを定義できます。このようなメソッドは、構成で baseClassForTests
として定義されたクラスに追加できます。次のコードは、テストケースの契約部分の例を示しています。
method GET()
r.method(r.GET());
method = GET
次のコードは、テストケースの基本クラス部分を示しています。
abstract class BaseMockMvcSpec extends Specification {
def setup() {
RestAssuredMockMvc.standaloneSetup(new PairIdController())
}
void isProperCorrelationId(Integer correlationId) {
assert correlationId == 123456
}
void isEmpty(String value) {
assert value == null
}
}
String と execute の両方を使用して連結を実行することはできません。例: header('Authorization', 'Bearer ' + execute('authToken()')) を呼び出すと不適切な結果が生じます。代わりに、header('Authorization', execute('authToken()')) を呼び出し、authToken() メソッドが必要なものをすべて返すようにします。 |
JSON から読み取られるオブジェクトの型は、JSON パスに応じて次のいずれかになります。
String
: JSON 内のString
値を指す場合。JSONArray
: JSON でList
を指している場合。Map
: JSON でMap
を指している場合。Number
: JSON 内のInteger
、Double
、その他の数値型を指す場合。Boolean
: JSON でBoolean
を指している場合。
契約のリクエスト部分では、body
をメソッドから取得するように指定できます。
コンシューマー側とプロデューサー側の両方を提供する必要があります。execute パーツは部分的なものではなく、全身に適用されます。 |
次の例は、JSON からオブジェクトを読み取る方法を示しています。
Contract contractDsl = Contract.make {
request {
method 'GET'
url '/something'
body(
$(c('foo'), p(execute('hashCode()')))
)
}
response {
status OK()
}
}
前述の例では、リクエスト本文で hashCode()
メソッドが呼び出されます。次のコードのようになります。
// given:
MockMvcRequestSpecification request = given()
.body(hashCode());
// when:
ResponseOptions response = given().spec(request)
.get("/something");
// then:
assertThat(response.statusCode()).isEqualTo(200);
レスポンスからリクエストを参照する
最良の状況は固定値を提供することですが、場合によってはレスポンス内でリクエストを参照する必要があります。
Groovy DSL で契約を作成する場合は、fromRequest()
メソッドを使用できます。これにより、HTTP リクエストから多数の要素を参照できます。次のオプションを使用できます。
fromRequest().url()
: リクエスト URL とクエリパラメーターを返します。fromRequest().query(String key)
: 指定された名前の最初のクエリパラメーターを返します。fromRequest().query(String key, int index)
: 指定された名前の n 番目のクエリパラメーターを返します。fromRequest().path()
: フルパスを返します。fromRequest().path(int index)
: n 番目のパス要素を返します。fromRequest().header(String key)
: 指定された名前の最初のヘッダーを返します。fromRequest().header(String key, int index)
: 指定された名前の n 番目のヘッダーを返します。fromRequest().body()
: 完全なリクエスト本文を返します。fromRequest().body(String jsonPath)
: JSON パスに一致するリクエストから要素を返します。
YAML 契約定義または Java 契約定義を使用する場合、これを実現するには、カスタム Spring Cloud Contract 関数で Handlebars (英語) {{{ }}}
表記を使用する必要があります。その場合、次のオプションを使用できます。
{{{ request.url }}}
: リクエスト URL とクエリパラメーターを返します。{{{ request.query.key.[index] }}}
: 指定された名前の n 番目のクエリパラメーターを返します。例:thing
のキーの場合、最初のエントリは{{{ request.query.thing.[0] }}}
です{{{ request.path }}}
: フルパスを返します。{{{ request.path.[index] }}}
: n 番目のパス要素を返します。例: 最初のエントリは`
{{{ request.path.[0] }}}{{{ request.headers.key }}}
: 指定された名前の最初のヘッダーを返します。{{{ request.headers.key.[index] }}}
: 指定された名前の n 番目のヘッダーを返します。{{{ request.body }}}
: 完全なリクエスト本文を返します。{{{ jsonpath this 'your.json.path' }}}
: JSON パスに一致するリクエストから要素を返します。例:$.here
の JSON パスの場合は、{{{ jsonpath this '$.here' }}}
を使用します
次の契約について考えてみましょう。
Contract contractDsl = Contract.make {
request {
method 'GET'
url('/api/v1/xxxx') {
queryParameters {
parameter('foo', 'bar')
parameter('foo', 'bar2')
}
}
headers {
header(authorization(), 'secret')
header(authorization(), 'secret2')
}
body(foo: 'bar', baz: 5)
}
response {
status OK()
headers {
header(authorization(), "foo ${fromRequest().header(authorization())} bar")
}
body(
url: fromRequest().url(),
path: fromRequest().path(),
pathIndex: fromRequest().path(1),
param: fromRequest().query('foo'),
paramIndex: fromRequest().query('foo', 1),
authorization: fromRequest().header('Authorization'),
authorization2: fromRequest().header('Authorization', 1),
fullBody: fromRequest().body(),
responseFoo: fromRequest().body('$.foo'),
responseBaz: fromRequest().body('$.baz'),
responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla",
rawUrl: fromRequest().rawUrl(),
rawPath: fromRequest().rawPath(),
rawPathIndex: fromRequest().rawPath(1),
rawParam: fromRequest().rawQuery('foo'),
rawParamIndex: fromRequest().rawQuery('foo', 1),
rawAuthorization: fromRequest().rawHeader('Authorization'),
rawAuthorization2: fromRequest().rawHeader('Authorization', 1),
rawResponseFoo: fromRequest().rawBody('$.foo'),
rawResponseBaz: fromRequest().rawBody('$.baz'),
rawResponseBaz2: "Bla bla ${fromRequest().rawBody('$.foo')} bla bla"
)
}
}
Contract contractDsl = Contract.make {
request {
method 'GET'
url('/api/v1/xxxx') {
queryParameters {
parameter('foo', 'bar')
parameter('foo', 'bar2')
}
}
headers {
header(authorization(), 'secret')
header(authorization(), 'secret2')
}
body(foo: "bar", baz: 5)
}
response {
status OK()
headers {
contentType(applicationJson())
}
body('''
{
"responseFoo": "{{{ jsonPath request.body '$.foo' }}}",
"responseBaz": {{{ jsonPath request.body '$.baz' }}},
"responseBaz2": "Bla bla {{{ jsonPath request.body '$.foo' }}} bla bla"
}
'''.toString())
}
}
request:
method: GET
url: /api/v1/xxxx
queryParameters:
foo:
- bar
- bar2
headers:
Authorization:
- secret
- secret2
body:
foo: bar
baz: 5
response:
status: 200
headers:
Authorization: "foo {{{ request.headers.Authorization.0 }}} bar"
body:
url: "{{{ request.url }}}"
path: "{{{ request.path }}}"
pathIndex: "{{{ request.path.1 }}}"
param: "{{{ request.query.foo }}}"
paramIndex: "{{{ request.query.foo.1 }}}"
authorization: "{{{ request.headers.Authorization.0 }}}"
authorization2: "{{{ request.headers.Authorization.1 }}"
fullBody: "{{{ request.body }}}"
responseFoo: "{{{ jsonpath this '$.foo' }}}"
responseBaz: "{{{ jsonpath this '$.baz' }}}"
responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla"
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.map;
class shouldReturnStatsForAUser implements Supplier<Contract> {
@Override
public Contract get() {
return Contract.make(c -> {
c.request(r -> {
r.method("POST");
r.url("/stats");
r.body(map().entry("name", r.anyAlphaUnicode()));
r.headers(h -> {
h.contentType(h.applicationJson());
});
});
c.response(r -> {
r.status(r.OK());
r.body(map()
.entry("text",
"Dear {{{jsonPath request.body '$.name'}}} thanks for your interested in drinking beer")
.entry("quantity", r.$(r.c(5), r.p(r.anyNumber()))));
r.headers(h -> {
h.contentType(h.applicationJson());
});
});
});
}
}
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
contract {
request {
method = method("POST")
url = url("/stats")
body(mapOf(
"name" to anyAlphaUnicode
))
headers {
contentType = APPLICATION_JSON
}
}
response {
status = OK
body(mapOf(
"text" to "Don't worry $\{fromRequest().body("$.name")} thanks for your interested in drinking beer",
"quantity" to v(c(5), p(anyNumber))
))
headers {
contentType = fromRequest().header(CONTENT_TYPE)
}
}
}
JUnit テスト生成を実行すると、次の例のようなテストが行われます。
// given:
MockMvcRequestSpecification request = given()
.header("Authorization", "secret")
.header("Authorization", "secret2")
.body("{\"foo\":\"bar\",\"baz\":5}");
// when:
ResponseOptions response = given().spec(request)
.queryParam("foo","bar")
.queryParam("foo","bar2")
.get("/api/v1/xxxx");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Authorization")).isEqualTo("foo secret bar");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['fullBody']").isEqualTo("{\"foo\":\"bar\",\"baz\":5}");
assertThatJson(parsedJson).field("['authorization']").isEqualTo("secret");
assertThatJson(parsedJson).field("['authorization2']").isEqualTo("secret2");
assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx");
assertThatJson(parsedJson).field("['param']").isEqualTo("bar");
assertThatJson(parsedJson).field("['paramIndex']").isEqualTo("bar2");
assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1");
assertThatJson(parsedJson).field("['responseBaz']").isEqualTo(5);
assertThatJson(parsedJson).field("['responseFoo']").isEqualTo("bar");
assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2");
assertThatJson(parsedJson).field("['responseBaz2']").isEqualTo("Bla bla bar bla bla");
ご覧のとおり、リクエストの要素はレスポンス内で適切に参照されています。
生成された WireMock スタブは次の例のようになります。
{
"request" : {
"urlPath" : "/api/v1/xxxx",
"method" : "POST",
"headers" : {
"Authorization" : {
"equalTo" : "secret2"
}
},
"queryParameters" : {
"foo" : {
"equalTo" : "bar2"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$[?(@.['baz'] == 5)]"
}, {
"matchesJsonPath" : "$[?(@.['foo'] == 'bar')]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"path\":\"{{{request.path}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"param\":\"{{{request.query.foo.[0]}}}\",\"pathIndex\":\"{{{request.path.[1]}}}\",\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"url\":\"{{{request.url}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\"}",
"headers" : {
"Authorization" : "{{{request.headers.Authorization.[0]}}};foo"
},
"transformers" : [ "response-template" ]
}
}
契約の request
部分に示されているようなリクエストを送信すると、次のレスポンス本文が送信されます。
{
"url" : "/api/v1/xxxx?foo=bar&foo=bar2",
"path" : "/api/v1/xxxx",
"pathIndex" : "v1",
"param" : "bar",
"paramIndex" : "bar2",
"authorization" : "secret",
"authorization2" : "secret2",
"fullBody" : "{\"foo\":\"bar\",\"baz\":5}",
"responseFoo" : "bar",
"responseBaz" : 5,
"responseBaz2" : "Bla bla bar bla bla"
}
この機能は、2.5.1 以降の WireMock バージョンでのみ動作します。Spring Cloud Contract Verifier は、WireMock の response-template レスポンストランスを使用します。Handlebars を使用して、Mustache {{{ }}} テンプレートを適切な値に変換します。さらに、次の 2 つのヘルパー関数を登録します。 |
escapejsonbody
: リクエスト本文を JSON に埋め込める形式でエスケープします。jsonpath
: 指定されたパラメーターについて、リクエスト本文内のオブジェクトを検索します。
Matchers セクションの動的プロパティ
協定 (英語) を使用している場合は、次の説明に馴染みがあるかもしれません。かなりの数のユーザーが、契約の本文と動的部分の設定を分離することに慣れています。
bodyMatchers
セクションは次の 2 つの理由で使用できます。
最終的にスタブに含める動的値を定義します。契約の
request
部分で設定できます。テストの結果を確認します。このセクションは、契約の
response
またはoutputMessage
側に存在します。
現在、Spring Cloud Contract Verifier は、次の一致の可能性がある JSON パスベースのマッチャーのみをサポートしています。
コード化された DSL
スタブの場合 (コンシューマー側のテスト):
byEquality()
: 提供された JSON パス内のコンシューマーのリクエストから取得された値は、契約で提供された値と等しい必要があります。byRegex(…)
: 提供された JSON パス内のコンシューマーのリクエストから取得された値は、正規表現と一致する必要があります。予想される一致値の型 (たとえば、asString()
、asLong()
など) を渡すこともできます。byDate()
: 提供された JSON パス内のコンシューマーのリクエストから取得された値は、ISO 日付値の正規表現と一致する必要があります。byTimestamp()
: 提供された JSON パス内のコンシューマーのリクエストから取得された値は、ISO DateTime 値の正規表現と一致する必要があります。byTime()
: 提供された JSON パス内のコンシューマーのリクエストから取得された値は、ISO 時間値の正規表現と一致する必要があります。
検証の場合 (プロデューサー側で生成されたテストで):
byEquality()
: 提供された JSON パス内のプロデューサーのレスポンスから取得された値は、契約で提供された値と等しくなければなりません。byRegex(…)
: 提供された JSON パス内のプロデューサーのレスポンスから取得された値は、正規表現と一致する必要があります。byDate()
: 提供された JSON パス内のプロデューサーのレスポンスから取得された値は、ISO 日付値の正規表現と一致する必要があります。byTimestamp()
: 提供された JSON パス内のプロデューサーのレスポンスから取得された値は、ISO DateTime 値の正規表現と一致する必要があります。byTime()
: 提供された JSON パス内のプロデューサーのレスポンスから取得された値は、ISO 時間値の正規表現と一致する必要があります。byType()
: 指定された JSON パス内のプロデューサーのレスポンスから取得された値は、契約内のレスポンスの本文で定義されている型と同じ型である必要があります。byType
はクロージャを受け取ることができ、その中でminOccurrence
とmaxOccurrence
を設定できます。リクエスト側では、クロージャを使用してコレクションのサイズをアサートする必要があります。こうすることで、フラット化されたコレクションのサイズを確認できます。非フラット化コレクションのサイズを確認するには、byCommand(…)
testMatcher
でカスタムメソッドを使用します。byCommand(…)
: 指定された JSON パス内のプロデューサーのレスポンスから取得された値は、入力として指定されたカスタムメソッドに渡されます。例:byCommand('thing($it)')
は、JSON パスに一致する値が渡されるthing
メソッドを呼び出します。JSON から読み取られるオブジェクトの型は、JSON パスに応じて次のいずれかになります。String
:String
値を指している場合。JSONArray
:List
を指している場合。Map
:Map
を指している場合。Number
:Integer
、Double
、または別の種類の番号を指している場合。Boolean
:Boolean
を指している場合。
byNull()
: 指定された JSON パスのレスポンスから取得された値は null である必要があります。
YAML
型の意味の詳細については、"Groovy" セクションを参照してください。 |
YAML の場合、マッチャーの構造は次の例のようになります。
- path: $.thing1
type: by_regex
value: thing2
regexType: as_string
あるいは、事前定義された正規表現 [only_alpha_unicode, number, any_boolean, ip_address, hostname, email, url, uuid, iso_date, iso_date_time, iso_time, iso_8601_with_offset, non_empty, non_blank]
のいずれかを使用する場合は、次の例のようなものを使用できます。
- path: $.thing1
type: by_regex
predefined: only_alpha_unicode
次のリストは、type
値の許可リストを示しています。
stubMatchers
の場合:by_equality
by_regex
by_date
by_timestamp
by_time
by_type
2 つの追加フィールド (
minOccurrence
およびmaxOccurrence
) が受け入れられます。
testMatchers
の場合:by_equality
by_regex
by_date
by_timestamp
by_time
by_type
2 つの追加フィールド (
minOccurrence
およびmaxOccurrence
) が受け入れられます。
by_command
by_null
正規表現がどの型に対応するかを regexType
フィールドで定義することもできます。次のリストは、許可される正規表現の種類を示しています。
as_integer
as_double
as_float
as_long
as_short
as_boolean
as_string
次の例を考えてみましょう。
Contract contractDsl = Contract.make {
request {
method 'GET'
urlPath '/get'
body([
duck : 123,
alpha : 'abc',
number : 123,
aBoolean : true,
date : '2017-01-01',
dateTime : '2017-01-01T01:23:45',
time : '01:02:34',
valueWithoutAMatcher: 'foo',
valueWithTypeMatch : 'string',
key : [
'complex.key': 'foo'
]
])
bodyMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()).asInteger())
jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
jsonPath('$.date', byDate())
jsonPath('$.dateTime', byTimestamp())
jsonPath('$.time', byTime())
jsonPath("\$.['key'].['complex.key']", byEquality())
}
headers {
contentType(applicationJson())
}
}
response {
status OK()
body([
duck : 123,
alpha : 'abc',
number : 123,
positiveInteger : 1234567890,
negativeInteger : -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
aBoolean : true,
date : '2017-01-01',
dateTime : '2017-01-01T01:23:45',
time : "01:02:34",
valueWithoutAMatcher : 'foo',
valueWithTypeMatch : 'string',
valueWithMin : [
1, 2, 3
],
valueWithMax : [
1, 2, 3
],
valueWithMinMax : [
1, 2, 3
],
valueWithMinEmpty : [],
valueWithMaxEmpty : [],
key : [
'complex.key': 'foo'
],
nullValue : null
])
bodyMatchers {
// asserts the jsonpath value against manual regex
jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
// asserts the jsonpath value against the provided value
jsonPath('$.duck', byEquality())
// asserts the jsonpath value against some default regex
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()).asInteger())
jsonPath('$.positiveInteger', byRegex(anInteger()).asInteger())
jsonPath('$.negativeInteger', byRegex(anInteger()).asInteger())
jsonPath('$.positiveDecimalNumber', byRegex(aDouble()).asDouble())
jsonPath('$.negativeDecimalNumber', byRegex(aDouble()).asDouble())
jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
// asserts vs inbuilt time related regex
jsonPath('$.date', byDate())
jsonPath('$.dateTime', byTimestamp())
jsonPath('$.time', byTime())
// asserts that the resulting type is the same as in response body
jsonPath('$.valueWithTypeMatch', byType())
jsonPath('$.valueWithMin', byType {
// results in verification of size of array (min 1)
minOccurrence(1)
})
jsonPath('$.valueWithMax', byType {
// results in verification of size of array (max 3)
maxOccurrence(3)
})
jsonPath('$.valueWithMinMax', byType {
// results in verification of size of array (min 1 & max 3)
minOccurrence(1)
maxOccurrence(3)
})
jsonPath('$.valueWithMinEmpty', byType {
// results in verification of size of array (min 0)
minOccurrence(0)
})
jsonPath('$.valueWithMaxEmpty', byType {
// results in verification of size of array (max 0)
maxOccurrence(0)
})
// will execute a method `assertThatValueIsANumber`
jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
jsonPath("\$.['key'].['complex.key']", byEquality())
jsonPath('$.nullValue', byNull())
}
headers {
contentType(applicationJson())
header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
}
}
}
request:
method: GET
urlPath: /get/1
headers:
Content-Type: application/json
cookies:
foo: 2
bar: 3
queryParameters:
limit: 10
offset: 20
filter: 'email'
sort: name
search: 55
age: 99
name: John.Doe
email: '[email protected] (英語) '
body:
duck: 123
alpha: "abc"
number: 123
aBoolean: true
date: "2017-01-01"
dateTime: "2017-01-01T01:23:45"
time: "01:02:34"
valueWithoutAMatcher: "foo"
valueWithTypeMatch: "string"
key:
"complex.key": 'foo'
nullValue: null
valueWithMin:
- 1
- 2
- 3
valueWithMax:
- 1
- 2
- 3
valueWithMinMax:
- 1
- 2
- 3
valueWithMinEmpty: []
valueWithMaxEmpty: []
matchers:
url:
regex: /get/[0-9]
# predefined:
# execute a method
#command: 'equals($it)'
queryParameters:
- key: limit
type: equal_to
value: 20
- key: offset
type: containing
value: 20
- key: sort
type: equal_to
value: name
- key: search
type: not_matching
value: '^[0-9]{2}$'
- key: age
type: not_matching
value: '^\\w*$'
- key: name
type: matching
value: 'John.*'
- key: hello
type: absent
cookies:
- key: foo
regex: '[0-9]'
- key: bar
command: 'equals($it)'
headers:
- key: Content-Type
regex: "application/json.*"
body:
- path: $.duck
type: by_regex
value: "[0-9]{3}"
- path: $.duck
type: by_equality
- path: $.alpha
type: by_regex
predefined: only_alpha_unicode
- path: $.alpha
type: by_equality
- path: $.number
type: by_regex
predefined: number
- path: $.aBoolean
type: by_regex
predefined: any_boolean
- path: $.date
type: by_date
- path: $.dateTime
type: by_timestamp
- path: $.time
type: by_time
- path: "$.['key'].['complex.key']"
type: by_equality
- path: $.nullvalue
type: by_null
- path: $.valueWithMin
type: by_type
minOccurrence: 1
- path: $.valueWithMax
type: by_type
maxOccurrence: 3
- path: $.valueWithMinMax
type: by_type
minOccurrence: 1
maxOccurrence: 3
response:
status: 200
cookies:
foo: 1
bar: 2
body:
duck: 123
alpha: "abc"
number: 123
aBoolean: true
date: "2017-01-01"
dateTime: "2017-01-01T01:23:45"
time: "01:02:34"
valueWithoutAMatcher: "foo"
valueWithTypeMatch: "string"
valueWithMin:
- 1
- 2
- 3
valueWithMax:
- 1
- 2
- 3
valueWithMinMax:
- 1
- 2
- 3
valueWithMinEmpty: []
valueWithMaxEmpty: []
key:
'complex.key': 'foo'
nulValue: null
matchers:
headers:
- key: Content-Type
regex: "application/json.*"
cookies:
- key: foo
regex: '[0-9]'
- key: bar
command: 'equals($it)'
body:
- path: $.duck
type: by_regex
value: "[0-9]{3}"
- path: $.duck
type: by_equality
- path: $.alpha
type: by_regex
predefined: only_alpha_unicode
- path: $.alpha
type: by_equality
- path: $.number
type: by_regex
predefined: number
- path: $.aBoolean
type: by_regex
predefined: any_boolean
- path: $.date
type: by_date
- path: $.dateTime
type: by_timestamp
- path: $.time
type: by_time
- path: $.valueWithTypeMatch
type: by_type
- path: $.valueWithMin
type: by_type
minOccurrence: 1
- path: $.valueWithMax
type: by_type
maxOccurrence: 3
- path: $.valueWithMinMax
type: by_type
minOccurrence: 1
maxOccurrence: 3
- path: $.valueWithMinEmpty
type: by_type
minOccurrence: 0
- path: $.valueWithMaxEmpty
type: by_type
maxOccurrence: 0
- path: $.duck
type: by_command
value: assertThatValueIsANumber($it)
- path: $.nullValue
type: by_null
value: null
headers:
Content-Type: application/json
前の例では、matchers
セクションで契約の動的部分を確認できます。リクエスト部分では、valueWithoutAMatcher
を除くすべてのフィールドで、スタブに含める正規表現の値が明示的に設定されていることがわかります。valueWithoutAMatcher
の場合、検証はマッチャーを使用しない場合と同じ方法で行われます。その場合、テストでは等価性チェックが実行されます。
bodyMatchers
セクションのレスポンス側についても、同様の方法で動的部分を定義します。唯一の違いは、byType
マッチャーも存在することです。検証エンジンは 4 つのフィールドをチェックして、テストからのレスポンスに JSON パスが指定されたフィールドと一致する値があるかどうか、レスポンス本文で定義されているものと同じ型であるかどうかを検証し、次のチェックに合格します (呼び出されるメソッド):
$.valueWithTypeMatch
の場合、エンジンは型が同じかどうかを確認します。$.valueWithMin
の場合、エンジンは型をチェックし、サイズが最小出現箇所以上であるかどうかをアサートします。$.valueWithMax
の場合、エンジンは型をチェックし、サイズが最大出現数以下であるかどうかをアサートします。$.valueWithMinMax
の場合、エンジンは型をチェックし、サイズが最小値と最大値の間にあるかどうかをアサートします。
結果のテストは次の例のようになります (and
セクションが自動生成されたアサーションとマッチャーからのアサーションを分離していることに注意してください)。
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\",\"key\":{\"complex.key\":\"foo\"}}");
// when:
ResponseOptions response = given().spec(request)
.get("/get");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['valueWithoutAMatcher']").isEqualTo("foo");
// and:
assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
assertThat(parsedJson.read("$.number", String.class)).matches("-?(\\d*\\.\\d+|\\d+)");
assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).as("$.valueWithMin").hasSizeGreaterThanOrEqualTo(1);
assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).as("$.valueWithMax").hasSizeLessThanOrEqualTo(3);
assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).as("$.valueWithMinMax").hasSizeBetween(1, 3);
assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).as("$.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0);
assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class);
assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0);
assertThatValueIsANumber(parsedJson.read("$.duck"));
assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo");
byCommand メソッドの場合、この例では assertThatValueIsANumber を呼び出していることに注意してください。このメソッドは、テスト基本クラスで定義するか、テストに静的にインポートする必要があります。byCommand 呼び出しが assertThatValueIsANumber(parsedJson.read("$.duck")); に変換されたことに注目してください。これは、エンジンがメソッド名を取得し、適切な JSON パスをパラメーターとして渡したことを意味します。 |
結果の WireMock スタブは次の例に示されています。
'''
{
"request" : {
"urlPath" : "/get",
"method" : "POST",
"headers" : {
"Content-Type" : {
"matches" : "application/json.*"
}
},
"bodyPatterns" : [ {
"matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]"
}, {
"matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]"
}, {
"matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]"
}, {
"matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]"
}, {
"matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]"
}, {
"matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]"
}, {
"matchesJsonPath" : "$[?(@.duck == 123)]"
}, {
"matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
}, {
"matchesJsonPath" : "$[?(@.alpha == 'abc')]"
}, {
"matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\d+|\\\\d+))/)]"
}, {
"matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]"
}, {
"matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]"
}, {
"matchesJsonPath" : "$[?(@.dateTime =~ /(([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
}, {
"matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
}, {
"matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithMin.size() >= 1)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithMax.size() <= 3)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithMinMax.size() >= 1 && @.valueWithMinMax.size() <= 3)]"
}, {
"matchesJsonPath" : "$[?(@.valueWithOccurrence.size() >= 4 && @.valueWithOccurrence.size() <= 4)]"
} ]
},
"response" : {
"status" : 200,
"body" : "{\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"number\\":123,\\"aBoolean\\":true,\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"time\\":\\"01:02:34\\",\\"valueWithoutAMatcher\\":\\"foo\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMin\\":[1,2,3],\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithOccurrence\\":[1,2,3,4]}",
"headers" : {
"Content-Type" : "application/json"
},
"transformers" : [ "response-template", "spring-cloud-contract" ]
}
}
'''
matcher を使用する場合、matcher が JSON パスでアドレス指定するリクエストとレスポンスの部分がアサーションから削除されます。コレクションを検証する場合は、コレクションのすべての要素に対してマッチャーを作成する必要があります。 |
次の例を考えてみましょう。
Contract.make {
request {
method 'GET'
url("/foo")
}
response {
status OK()
body(events: [[
operation : 'EXPORT',
eventId : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
status : 'OK'
], [
operation : 'INPUT_PROCESSING',
eventId : '3bb4ac82-6652-462f-b6d1-75e424a0024a',
status : 'OK'
]
]
)
bodyMatchers {
jsonPath('$.events[0].operation', byRegex('.+'))
jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$'))
jsonPath('$.events[0].status', byRegex('.+'))
}
}
}
前述のコードにより、次のテストが作成されます (コードブロックにはアサーションセクションのみが示されています)。
and:
DocumentContext parsedJson = JsonPath.parse(response.body.asString())
assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("16f1ed75-0bcc-4f0d-a04d-3121798faf99")
assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("EXPORT")
assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("INPUT_PROCESSING")
assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("3bb4ac82-6652-462f-b6d1-75e424a0024a")
assertThatJson(parsedJson).array("['events']").contains("['status']").isEqualTo("OK")
and:
assertThat(parsedJson.read("\$.events[0].operation", String.class)).matches(".+")
assertThat(parsedJson.read("\$.events[0].eventId", String.class)).matches("^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\$")
assertThat(parsedJson.read("\$.events[0].status", String.class)).matches(".+")
アサーションの形式が正しくないことに注意してください。配列の最初の要素のみがアサートされました。これを修正するには、アサーションを $.events
コレクション全体に適用し、byCommand(…)
メソッドでアサートします。