動的プロパティ

契約には、タイムスタンプ、ID などのいくつかの動的プロパティを含めることができます。スタブによって一致するように、常に同じ時刻値を返すようにコンシューマーにクロックをスタブ化させることは望ましくありません。

Groovy DSL の場合、契約内の動的部分を 2 つの方法で提供できます。本体に直接渡すか、bodyMatchers という別のセクションに設定します。

2.0.0 より前は、これらは testMatchers および stubMatchers を使用して設定されていました。詳細については、移行ガイド [GitHub] (英語) を参照してください。

YAML の場合、matchers セクションのみを使用できます。

matchers 内のエントリは、ペイロードの既存の要素を参照する必要があります。詳細については、この課題 [GitHub] (英語) を参照してください。

ボディ内部の動的特性

このセクションは、コード化 DSL (Groovy、Java など) に対してのみ有効です。同様の機能の YAML の例については、"Matchers セクションの動的プロパティ" セクションを参照してください。

本体内のプロパティは、value メソッドを使用して設定するか、Groovy マップ表記法を使用する場合は $() を使用して設定できます。次の例は、value メソッドを使用して動的プロパティを設定する方法を示しています。

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 は一致します。さらにいくつかの既知の制限もあります。

次の例は、正規表現を使用してリクエストを作成する方法を示しています。

Groovy
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'
		}
	}
}
Java
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");
		});
	});
});
Kotlin
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);

次の例は、これらのメソッドを参照する方法を示しています。

Groovy
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'))
		])
	}
}
Kotlin
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 側

次の例は、オプションのパラメーターを指定する方法を示しています。

Groovy
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")))
		)
	}
}
Java
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")))));
	});
});
Kotlin
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 を使用する場合、前の例から次のテストが生成されます。

Groovy
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 として定義されたクラスに追加できます。次のコードは、テストケースの契約部分の例を示しています。

Groovy
method GET()
Java
r.method(r.GET());
Kotlin
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 内の IntegerDouble、その他の数値型を指す場合。

  • 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' }}} を使用します

次の契約について考えてみましょう。

Groovy
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())
	}
}
YAML
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"
Java
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());
				});
			});
		});
	}

}
Kotlin
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 パスに応じて次のいずれかになります。

    • StringString 値を指している場合。

    • JSONArrayList を指している場合。

    • MapMap を指している場合。

    • NumberIntegerDouble、または別の種類の番号を指している場合。

    • BooleanBoolean を指している場合。

  • 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

次の例を考えてみましょう。

Groovy
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}'))))
		}
	}
}
YAML
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(…​) メソッドでアサートします。