HTTP 契約

このページでは、契約の最も重要な HTTP 関連部分について説明します。

HTTP のトップレベル要素

契約定義の最上位クロージャで次のメソッドを呼び出すことができます。

  • request: 必須

  • response : 必須

  • priority: オプション

次の例は、HTTP リクエスト 契約を定義する方法を示しています。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	// Definition of HTTP request part of the contract
	// (this can be a valid request or invalid depending
	// on type of contract being specified).
	request {
		method GET()
		url "/foo"
		//...
	}

	// Definition of HTTP response part of the contract
	// (a service implementing this contract should respond
	// with following response after receiving request
	// specified in "request" part above).
	response {
		status 200
		//...
	}

	// Contract priority, which can be used for overriding
	// contracts (1 is highest). Priority is optional.
	priority 1
}
YAML
priority: 8
request:
...
response:
...
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	// Definition of HTTP request part of the contract
	// (this can be a valid request or invalid depending
	// on type of contract being specified).
	c.request(r -> {
		r.method(r.GET());
		r.url("/foo");
		// ...
	});

	// Definition of HTTP response part of the contract
	// (a service implementing this contract should respond
	// with following response after receiving request
	// specified in "request" part above).
	c.response(r -> {
		r.status(200);
		// ...
	});

	// Contract priority, which can be used for overriding
	// contracts (1 is highest). Priority is optional.
	c.priority(1);
});
Kotlin
contract {
    // Definition of HTTP request part of the contract
    // (this can be a valid request or invalid depending
    // on type of contract being specified).
    request {
        method = GET
        url = url("/foo")
        // ...
    }

    // Definition of HTTP response part of the contract
    // (a service implementing this contract should respond
    // with following response after receiving request
    // specified in "request" part above).
    response {
        status = OK
        // ...
    }

    // Contract priority, which can be used for overriding
    // contracts (1 is highest). Priority is optional.
    priority = 1
}
契約の優先順位を高くしたい場合は、priority タグまたはメソッドに小さい数値を渡す必要があります。例: 5 の値を持つ priority は、10 の値を持つ priority よりも高い優先順位を持ちます。

HTTP リクエスト

HTTP プロトコルでは、リクエストにメソッドと URL のみを指定する必要があります。契約のリクエスト定義にも同じ情報が必須です。

次の例は、リクエストの契約を示しています。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	request {
		// HTTP request method (GET/POST/PUT/DELETE).
		method 'GET'

		// Path component of request URL is specified as follows.
		urlPath('/users')
	}

	response {
		//...
		status 200
	}
}
YAML
method: PUT
url: /foo
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		// HTTP request method (GET/POST/PUT/DELETE).
		r.method("GET");

		// Path component of request URL is specified as follows.
		r.urlPath("/users");
	});

	c.response(r -> {
		// ...
		r.status(200);
	});
});
Kotlin
contract {
    request {
        // HTTP request method (GET/POST/PUT/DELETE).
        method = method("GET")

        // Path component of request URL is specified as follows.
        urlPath = path("/users")
    }
    response {
        // ...
        status = code(200)
    }
}

相対的な url ではなく絶対的な url を指定することもできますが、テストがホストに依存しないため、urlPath を使用することをお勧めします。

次の例では、url を使用しています。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	request {
		method 'GET'

		// Specifying `url` and `urlPath` in one contract is illegal.
		url('http://localhost:8888/users')
	}

	response {
		//...
		status 200
	}
}
YAML
request:
  method: PUT
  urlPath: /foo
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		r.method("GET");

		// Specifying `url` and `urlPath` in one contract is illegal.
		r.url("http://localhost:8888/users");
	});

	c.response(r -> {
		// ...
		r.status(200);
	});
});
Kotlin
contract {
    request {
        method = GET

        // Specifying `url` and `urlPath` in one contract is illegal.
        url("http://localhost:8888/users")
    }
    response {
        // ...
        status = OK
    }
}

次の例 ( urlPath を使用) に示すように、request にはクエリパラメーターが含まれる場合があります。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	request {
		//...
		method GET()

		urlPath('/users') {

			// Each parameter is specified in form
			// `'paramName' : paramValue` where parameter value
			// may be a simple literal or one of matcher functions,
			// all of which are used in this example.
			queryParameters {

				// If a simple literal is used as value
				// default matcher function is used (equalTo)
				parameter 'limit': 100

				// `equalTo` function simply compares passed value
				// using identity operator (==).
				parameter 'filter': equalTo("email")

				// `containing` function matches strings
				// that contains passed substring.
				parameter 'gender': value(consumer(containing("[mf]")), producer('mf'))

				// `matching` function tests parameter
				// against passed regular expression.
				parameter 'offset': value(consumer(matching("[0-9]+")), producer(123))

				// `notMatching` functions tests if parameter
				// does not match passed regular expression.
				parameter 'loginStartsWith': value(consumer(notMatching(".{0,2}")), producer(3))
			}
		}

		//...
	}

	response {
		//...
		status 200
	}
}
YAML
request:
...
queryParameters:
  a: b
  b: c
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		// ...
		r.method(r.GET());

		r.urlPath("/users", u -> {

			// Each parameter is specified in form
			// `'paramName' : paramValue` where parameter value
			// may be a simple literal or one of matcher functions,
			// all of which are used in this example.
			u.queryParameters(q -> {

				// If a simple literal is used as value
				// default matcher function is used (equalTo)
				q.parameter("limit", 100);

				// `equalTo` function simply compares passed value
				// using identity operator (==).
				q.parameter("filter", r.equalTo("email"));

				// `containing` function matches strings
				// that contains passed substring.
				q.parameter("gender", r.value(r.consumer(r.containing("[mf]")), r.producer("mf")));

				// `matching` function tests parameter
				// against passed regular expression.
				q.parameter("offset", r.value(r.consumer(r.matching("[0-9]+")), r.producer(123)));

				// `notMatching` functions tests if parameter
				// does not match passed regular expression.
				q.parameter("loginStartsWith", r.value(r.consumer(r.notMatching(".{0,2}")), r.producer(3)));
			});
		});

		// ...
	});

	c.response(r -> {
		// ...
		r.status(200);
	});
});
Kotlin
contract {
    request {
        // ...
        method = GET

        // Each parameter is specified in form
        // `'paramName' : paramValue` where parameter value
        // may be a simple literal or one of matcher functions,
        // all of which are used in this example.
        urlPath = path("/users") withQueryParameters {
            // If a simple literal is used as value
            // default matcher function is used (equalTo)
            parameter("limit", 100)

            // `equalTo` function simply compares passed value
            // using identity operator (==).
            parameter("filter", equalTo("email"))

            // `containing` function matches strings
            // that contains passed substring.
            parameter("gender", value(consumer(containing("[mf]")), producer("mf")))

            // `matching` function tests parameter
            // against passed regular expression.
            parameter("offset", value(consumer(matching("[0-9]+")), producer(123)))

            // `notMatching` functions tests if parameter
            // does not match passed regular expression.
            parameter("loginStartsWith", value(consumer(notMatching(".{0,2}")), producer(3)))
        }

        // ...
    }
    response {
        // ...
        status = code(200)
    }
}
契約にクエリパラメーターが欠落している場合、クエリパラメーターが欠落している場合にリクエストが一致すると期待されるわけではありません。それはまったく逆で、リクエストを照合するためにクエリパラメーターが存在する必要はないということです。

次の例に示すように、request には追加のリクエストヘッダーを含めることができます。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	request {
		//...
		method GET()
		url "/foo"

		// Each header is added in form `'Header-Name' : 'Header-Value'`.
		// there are also some helper methods
		headers {
			header 'key': 'value'
			contentType(applicationJson())
		}

		//...
	}

	response {
		//...
		status 200
	}
}
YAML
request:
...
headers:
  foo: bar
  fooReq: baz
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		// ...
		r.method(r.GET());
		r.url("/foo");

		// Each header is added in form `'Header-Name' : 'Header-Value'`.
		// there are also some helper methods
		r.headers(h -> {
			h.header("key", "value");
			h.contentType(h.applicationJson());
		});

		// ...
	});

	c.response(r -> {
		// ...
		r.status(200);
	});
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url = url("/foo")

        // Each header is added in form `'Header-Name' : 'Header-Value'`.
        // there are also some helper variables
        headers {
            header("key", "value")
            contentType = APPLICATION_JSON
        }

        // ...
    }
    response {
        // ...
        status = OK
    }
}

次の例に示すように、request には追加のリクエスト Cookie が含まれる場合があります。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	request {
		//...
		method GET()
		url "/foo"

		// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
		// there are also some helper methods
		cookies {
			cookie 'key': 'value'
			cookie('another_key', 'another_value')
		}

		//...
	}

	response {
		//...
		status 200
	}
}
YAML
request:
...
cookies:
  foo: bar
  fooReq: baz
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		// ...
		r.method(r.GET());
		r.url("/foo");

		// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
		// there are also some helper methods
		r.cookies(ck -> {
			ck.cookie("key", "value");
			ck.cookie("another_key", "another_value");
		});

		// ...
	});

	c.response(r -> {
		// ...
		r.status(200);
	});
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url = url("/foo")

        // Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
        // there are also some helper methods
        cookies {
            cookie("key", "value")
            cookie("another_key", "another_value")
        }

        // ...
    }

    response {
        // ...
        status = code(200)
    }
}

次の例に示すように、request にはリクエスト本文が含まれる場合があります。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	request {
		//...
		method GET()
		url "/foo"

		// Currently only JSON format of request body is supported.
		// Format will be determined from a header or body's content.
		body '''{ "login" : "john", "name": "John The Contract" }'''
	}

	response {
		//...
		status 200
	}
}
YAML
request:
...
body:
  foo: bar
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		// ...
		r.method(r.GET());
		r.url("/foo");

		// Currently only JSON format of request body is supported.
		// Format will be determined from a header or body's content.
		r.body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }");
	});

	c.response(r -> {
		// ...
		r.status(200);
	});
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url = url("/foo")

        // Currently only JSON format of request body is supported.
        // Format will be determined from a header or body's content.
        body = body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }")
    }
    response {
        // ...
        status = OK
    }
}

request にはマルチパート要素を含めることができます。マルチパート要素を含めるには、次の例に示すように、multipart メソッド / セクションを使用します。

Groovy
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
	request {
		method 'PUT'
		url '/multipart'
		headers {
			contentType('multipart/form-data;boundary=AaB03x')
		}
		multipart(
				// key (parameter name), value (parameter value) pair
				formParameter: $(c(regex('".+"')), p('"formParameterValue"')),
				someBooleanParameter: $(c(regex(anyBoolean())), p('true')),
				// a named parameter (e.g. with `file` name) that represents file with
				// `name` and `content`. You can also call `named("fileName", "fileContent")`
				file: named(
						// name of the file
						name: $(c(regex(nonEmpty())), p('filename.csv')),
						// content of the file
						content: $(c(regex(nonEmpty())), p('file content')),
						// content type for the part
						contentType: $(c(regex(nonEmpty())), p('application/json')))
		)
	}
	response {
		status OK()
	}
}
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
	request {
		method "PUT"
		url "/multipart"
		headers {
			contentType('multipart/form-data;boundary=AaB03x')
		}
		multipart(
				file: named(
						name: value(stub(regex('.+')), test('file')),
						content: value(stub(regex('.+')), test([100, 117, 100, 97] as byte[]))
				)
		)
	}
	response {
		status 200
	}
}
YAML
request:
  method: PUT
  url: /multipart
  headers:
    Content-Type: multipart/form-data;boundary=AaB03x
  multipart:
    params:
      # key (parameter name), value (parameter value) pair
      formParameter: '"formParameterValue"'
      someBooleanParameter: true
    named:
      - paramName: file
        fileName: filename.csv
        fileContent: file content
  matchers:
    multipart:
      params:
        - key: formParameter
          regex: ".+"
        - key: someBooleanParameter
          predefined: any_boolean
      named:
        - paramName: file
          fileName:
            predefined: non_empty
          fileContent:
            predefined: non_empty
response:
  status: 200
Java
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.Request;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;

class contract_multipart implements Supplier<Collection<Contract>> {

	private static Map<String, DslProperty> namedProps(Request r) {
		Map<String, DslProperty> map = new HashMap<>();
		// name of the file
		map.put("name", r.$(r.c(r.regex(r.nonEmpty())), r.p("filename.csv")));
		// content of the file
		map.put("content", r.$(r.c(r.regex(r.nonEmpty())), r.p("file content")));
		// content type for the part
		map.put("contentType", r.$(r.c(r.regex(r.nonEmpty())), r.p("application/json")));
		return map;
	}

	@Override
	public Collection<Contract> get() {
		return Collections.singletonList(Contract.make(c -> {
			c.request(r -> {
				r.method("PUT");
				r.url("/multipart");
				r.headers(h -> {
					h.contentType("multipart/form-data;boundary=AaB03x");
				});
				r.multipart(ContractVerifierUtil.map()
						// key (parameter name), value (parameter value) pair
						.entry("formParameter", r.$(r.c(r.regex("\".+\"")), r.p("\"formParameterValue\"")))
						.entry("someBooleanParameter", r.$(r.c(r.regex(r.anyBoolean())), r.p("true")))
						// a named parameter (e.g. with `file` name) that represents file
						// with
						// `name` and `content`. You can also call `named("fileName",
						// "fileContent")`
						.entry("file", r.named(namedProps(r))));
			});
			c.response(r -> {
				r.status(r.OK());
			});
		}));
	}

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        method = PUT
        url = url("/multipart")
        multipart {
            field("formParameter", value(consumer(regex("\".+\"")), producer("\"formParameterValue\"")))
            field("someBooleanParameter", value(consumer(anyBoolean), producer("true")))
            field("file",
                named(
                    // name of the file
                    value(consumer(regex(nonEmpty)), producer("filename.csv")),
                    // content of the file
                    value(consumer(regex(nonEmpty)), producer("file content")),
                    // content type for the part
                    value(consumer(regex(nonEmpty)), producer("application/json"))
                )
            )
        }
        headers {
            contentType = "multipart/form-data;boundary=AaB03x"
        }
    }
    response {
        status = OK
    }
}

前の例では、次の 2 つの方法のいずれかでパラメーターを定義しました。

コード化された DSL
  • マップ表記を使用して直接、値を動的プロパティ ( formParameter: $(consumer(…​), producer(…​)) など) にすることができます。

  • named(…​) メソッドを使用すると、名前付きパラメーターを設定できます。名前付きパラメーターで name および content を設定できます。これを呼び出すには、named("fileName", "fileContent") などの 2 つの引数を持つメソッドを使用するか、named(name: "fileName", content: "fileContent") などのマップ表記を使用します。

YAML
  • マルチパートパラメーターは multipart.params セクションで設定されます。

  • 名前付きパラメーター (特定のパラメーター名の fileName および fileContent ) は、multipart.named セクションで設定できます。このセクションには、paramName (パラメーターの名前)、fileName (ファイルの名前)、fileContent (ファイルの内容) フィールドが含まれています。

  • 動的ビットは matchers.multipart セクションで設定できます。

    • パラメーターには、regex または predefined 正規表現を受け入れることができる params セクションを使用します。

    • 名前付きパラメーターの場合は、最初に paramName でパラメーター名を定義する named セクションを使用します。その後、fileName または fileContent のパラメーター化を regex または predefined 正規表現で渡すことができます。

named(…​) セクションでは、常に value(producer(…​), consumer(…​)) 呼び出しのペアを追加する必要があります。value(producer(…​)) のみ、file(…​) のみなどの DSL プロパティを設定するだけでは機能しません。詳細については、この課題 [GitHub] (英語) を確認してください。

前の例の契約から、生成されたテストとスタブは次のようになります。

Test
// given:
  MockMvcRequestSpecification request = given()
    .header("Content-Type", "multipart/form-data;boundary=AaB03x")
    .param("formParameter", "\"formParameterValue\"")
    .param("someBooleanParameter", "true")
    .multiPart("file", "filename.csv", "file content".getBytes());

 // when:
  ResponseOptions response = given().spec(request)
    .put("/multipart");

 // then:
  assertThat(response.statusCode()).isEqualTo(200);
Stub
			'''
{
  "request" : {
	"url" : "/multipart",
	"method" : "PUT",
	"headers" : {
	  "Content-Type" : {
		"matches" : "multipart/form-data;boundary=AaB03x.*"
	  }
	},
	"bodyPatterns" : [ {
		"matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n\\".+\\"\\r?\\n--.*"
  		}, {
    			"matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n(true|false)\\r?\\n--.*"
  		}, {			
	  "matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n[\\\\S\\\\s]+\\r?\\n--.*"
	} ]
  },
  "response" : {
	"status" : 200,
	"transformers" : [ "response-template", "foo-transformer" ]
  }
}
	'''

HTTP レスポンス

レスポンスには HTTP ステータスコードが含まれている必要があり、その他の情報が含まれる場合もあります。次のコードは例を示しています。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
	request {
		//...
		method GET()
		url "/foo"
	}
	response {
		// Status code sent by the server
		// in response to request specified above.
		status OK()
	}
}
YAML
response:
...
status: 200
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
	c.request(r -> {
		// ...
		r.method(r.GET());
		r.url("/foo");
	});
	c.response(r -> {
		// Status code sent by the server
		// in response to request specified above.
		r.status(r.OK());
	});
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url =url("/foo")
    }
    response {
        // Status code sent by the server
        // in response to request specified above.
        status = OK
    }
}

ステータスの他に、レスポンスにはヘッダー、Cookie、本文が含まれる場合があります。これらはリクエストと同じ方法で指定されます ( HTTP リクエストを参照)。

Groovy DSL では、org.springframework.cloud.contract.spec.internal.HttpStatus メソッドを参照して、数字の代わりに意味のあるステータスを提供できます。例: ステータス 200 に対して OK() を呼び出すか、400 に対して BAD_REQUEST() を呼び出すことができます。

HTTP の XML サポート

HTTP 契約の場合、リクエストおよびレスポンス本文での XML の使用もサポートされています。XML 本文は、body 要素内で String または GString として渡す必要があります。また、ボディマッチャーはリクエストとレスポンスの両方に提供できます。jsonPath(…​) メソッドの代わりに、目的の xPath を最初の引数として指定し、適切な MatchingType を 2 番目の引数として指定して、org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath メソッドを使用する必要があります。byType() を除くすべてのボディマッチャーがサポートされています。

次の例は、レスポンス本文に XML を含む Groovy DSL 契約を示しています。

Groovy
					Contract.make {
						request {
							method GET()
							urlPath '/get'
							headers {
								contentType(applicationXml())
							}
						}
						response {
							status(OK())
							headers {
								contentType(applicationXml())
							}
							body """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<key><complex>foo</complex></key>
</test>"""
							bodyMatchers {
								xPath('/test/duck/text()', byRegex("[0-9]{3}"))
								xPath('/test/duck/text()', byCommand('equals($it)'))
								xPath('/test/duck/xxx', byNull())
								xPath('/test/duck/text()', byEquality())
								xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
								xPath('/test/alpha/text()', byEquality())
								xPath('/test/number/text()', byRegex(number()))
								xPath('/test/date/text()', byDate())
								xPath('/test/dateTime/text()', byTimestamp())
								xPath('/test/time/text()', byTime())
								xPath('/test/*/complex/text()', byEquality())
								xPath('/test/duck/@type', byEquality())
							}
						}
					}
					Contract.make {
						request {
							method GET()
							urlPath '/get'
							headers {
								contentType(applicationXml())
							}
						}
						response {
							status(OK())
							headers {
								contentType(applicationXml())
							}
							body """
<ns1:test xmlns:ns1="http://demo.com/testns">
 <ns1:header>
    <duck-bucket type='bigbucket'>
      <duck>duck5150</duck>
    </duck-bucket>
</ns1:header>
</ns1:test>
"""
							bodyMatchers {
								xPath('/test/duck/text()', byRegex("[0-9]{3}"))
								xPath('/test/duck/text()', byCommand('equals($it)'))
								xPath('/test/duck/xxx', byNull())
								xPath('/test/duck/text()', byEquality())
								xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
								xPath('/test/alpha/text()', byEquality())
								xPath('/test/number/text()', byRegex(number()))
								xPath('/test/date/text()', byDate())
								xPath('/test/dateTime/text()', byTimestamp())
								xPath('/test/time/text()', byTime())
								xPath('/test/duck/@type', byEquality())
							}
						}
					}
					Contract.make {
						request {
							method GET()
							urlPath '/get'
							headers {
								contentType(applicationXml())
							}
						}
						response {
							status(OK())
							headers {
								contentType(applicationXml())
							}
							body """
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header>
      <RsHeader xmlns="http://schemas.xmlsoap.org/soap/custom">
         <MsgSeqId>1234</MsgSeqId>
      </RsHeader>
   </SOAP-ENV:Header>
</SOAP-ENV:Envelope>
"""
							bodyMatchers {
								xPath('//*[local-name()=\'RsHeader\' and namespace-uri()=\'http://schemas.xmlsoap.org/soap/custom\']/*[local-name()=\'MsgSeqId\']/text()', byEquality())
							}
						}
					}
					Contract.make {
						request {
							method GET()
							urlPath '/get'
							headers {
								contentType(applicationXml())
							}
						}
						response {
							status(OK())
							headers {
								contentType(applicationXml())
							}
							body """
<ns1:customer xmlns:ns1="http://demo.com/customer" xmlns:addr="http://demo.com/address">
	<email>[email protected] (英語)  </email>
	<contact-info xmlns="http://demo.com/contact-info">
		<name>Krombopulous</name>
		<address>
			<addr:gps>
				<lat>51</lat>
				<addr:lon>50</addr:lon>
			</addr:gps>
		</address>
	</contact-info>
</ns1:customer>
"""
						}
					}
YAML
request:
  method: GET
  url: /getymlResponse
  headers:
    Content-Type: application/xml
  body: |
    <test>
    <duck type='xtype'>123</duck>
    <alpha>abc</alpha>
    <list>
    <elem>abc</elem>
    <elem>def</elem>
    <elem>ghi</elem>
    </list>
    <number>123</number>
    <aBoolean>true</aBoolean>
    <date>2017-01-01</date>
    <dateTime>2017-01-01T01:23:45</dateTime>
    <time>01:02:34</time>
    <valueWithoutAMatcher>foo</valueWithoutAMatcher>
    <valueWithTypeMatch>string</valueWithTypeMatch>
    <key><complex>foo</complex></key>
    </test>
  matchers:
    body:
      - path: /test/duck/text()
        type: by_regex
        value: "[0-9]{10}"
      - path: /test/duck/text()
        type: by_equality
      - path: /test/time/text()
        type: by_time
response:
  status: 200
  headers:
    Content-Type: application/xml
  body: |
    <test>
    <duck type='xtype'>123</duck>
    <alpha>abc</alpha>
    <list>
    <elem>abc</elem>
    <elem>def</elem>
    <elem>ghi</elem>
    </list>
    <number>123</number>
    <aBoolean>true</aBoolean>
    <date>2017-01-01</date>
    <dateTime>2017-01-01T01:23:45</dateTime>
    <time>01:02:34</time>
    <valueWithoutAMatcher>foo</valueWithoutAMatcher>
    <valueWithTypeMatch>string</valueWithTypeMatch>
    <key><complex>foo</complex></key>
    </test>
  matchers:
    body:
      - path: /test/duck/text()
        type: by_regex
        value: "[0-9]{10}"
      - path: /test/duck/text()
        type: by_command
        value: "test($it)"
      - path: /test/duck/xxx
        type: by_null
      - path: /test/duck/text()
        type: by_equality
      - path: /test/time/text()
        type: by_time
Java
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;

class contract_xml implements Supplier<Contract> {

	@Override
	public Contract get() {
		return Contract.make(c -> {
			c.request(r -> {
				r.method(r.GET());
				r.urlPath("/get");
				r.headers(h -> {
					h.contentType(h.applicationXml());
				});
			});
			c.response(r -> {
				r.status(r.OK());
				r.headers(h -> {
					h.contentType(h.applicationXml());
				});
				r.body("<test>\n" + "<duck type='xtype'>123</duck>\n" + "<alpha>abc</alpha>\n" + "<list>\n"
						+ "<elem>abc</elem>\n" + "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
						+ "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n" + "<date>2017-01-01</date>\n"
						+ "<dateTime>2017-01-01T01:23:45</dateTime>\n" + "<time>01:02:34</time>\n"
						+ "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n" + "<key><complex>foo</complex></key>\n"
						+ "</test>");
				r.bodyMatchers(m -> {
					m.xPath("/test/duck/text()", m.byRegex("[0-9]{3}"));
					m.xPath("/test/duck/text()", m.byCommand("equals($it)"));
					m.xPath("/test/duck/xxx", m.byNull());
					m.xPath("/test/duck/text()", m.byEquality());
					m.xPath("/test/alpha/text()", m.byRegex(r.onlyAlphaUnicode()));
					m.xPath("/test/alpha/text()", m.byEquality());
					m.xPath("/test/number/text()", m.byRegex(r.number()));
					m.xPath("/test/date/text()", m.byDate());
					m.xPath("/test/dateTime/text()", m.byTimestamp());
					m.xPath("/test/time/text()", m.byTime());
					m.xPath("/test/*/complex/text()", m.byEquality());
					m.xPath("/test/duck/@type", m.byEquality());
				});
			});
		});
	};

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        method = GET
        urlPath = path("/get")
        headers {
            contentType = APPLICATION_XML
        }
    }
    response {
        status = OK
        headers {
            contentType =APPLICATION_XML
        }
        body = body("<test>\n" + "<duck type='xtype'>123</duck>\n"
                + "<alpha>abc</alpha>\n" + "<list>\n" + "<elem>abc</elem>\n"
                + "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
                + "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n"
                + "<date>2017-01-01</date>\n"
                + "<dateTime>2017-01-01T01:23:45</dateTime>\n"
                + "<time>01:02:34</time>\n"
                + "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n"
                + "<key><complex>foo</complex></key>\n" + "</test>")
        bodyMatchers {
            xPath("/test/duck/text()", byRegex("[0-9]{3}"))
            xPath("/test/duck/text()", byCommand("equals(\$it)"))
            xPath("/test/duck/xxx", byNull)
            xPath("/test/duck/text()", byEquality)
            xPath("/test/alpha/text()", byRegex(onlyAlphaUnicode))
            xPath("/test/alpha/text()", byEquality)
            xPath("/test/number/text()", byRegex(number))
            xPath("/test/date/text()", byDate)
            xPath("/test/dateTime/text()", byTimestamp)
            xPath("/test/time/text()", byTime)
            xPath("/test/*/complex/text()", byEquality)
            xPath("/test/duck/@type", byEquality)
        }
    }
}

次の例は、レスポンス本文内の XML に対して自動生成されたテストを示しています。

@Test
public void validate_xmlMatches() throws Exception {
	// given:
	MockMvcRequestSpecification request = given()
				.header("Content-Type", "application/xml");

	// when:
	ResponseOptions response = given().spec(request).get("/get");

	// then:
	assertThat(response.statusCode()).isEqualTo(200);
	// and:
	DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance()
					.newDocumentBuilder();
	Document parsedXml = documentBuilder.parse(new InputSource(
				new StringReader(response.getBody().asString())));
	// and:
	assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc");
	assertThat(valueFromXPath(parsedXml,"/test/list/elem[2]/text()")).isEqualTo("def");
	assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]\{3}");
	assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull();
	assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\p\{L}]*");
	assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo");
	assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype");
	}

名前空間の XML サポート

名前空間付き XML がサポートされています。ただし、名前空間コンテンツの選択に使用される XPath 式は更新する必要があります。

次の明示的に名前空間が設定された XML ドキュメントを考えてみましょう。

<ns1:customer xmlns:ns1="http://demo.com/customer">
    <email>[email protected] (英語)  </email>
</ns1:customer>

メールアドレスを選択するための XPath 式は、/ns1:customer/email/text() です。

修飾されていない式 (/customer/email/text()) は "" になるため注意してください。

非修飾の名前空間を使用するコンテンツの場合、式はより冗長になります。修飾されていない名前空間を使用する次の XML ドキュメントを考えてみましょう。

<customer xmlns="http://demo.com/customer">
    <email>[email protected] (英語)  </email>
</customer>

メールアドレスを選択するための XPath 式は次のとおりです

*/[local-name()='customer' and namespace-uri()='http://demo.com/customer']/*[local-name()='email']/text()
修飾されていない式 (/customer/email/text() または */[local-name()='customer' and namespace-uri()='http://demo.com/customer']/email/text()) は "" になるため注意してください。子要素も local-name 構文で参照する必要があります。

一般的な名前空間ノード式の構文

  • 修飾された名前空間を使用するノード:

/<node-name>
  • 修飾されていない名前空間を使用および定義しているノード:

/*[local-name=()='<node-name>' and namespace-uri=()='<namespace-uri>']
場合によっては、namespace_uri 部分を省略できますが、省略するとあいまいさが生じる可能性があります。
  • 修飾されていない名前空間を使用するノード (その祖先の 1 つが xmlns 属性を定義している):

/*[local-name=()='<node-name>']

非同期サポート

サーバー側で非同期通信を使用する場合 (コントローラーが CallableDeferredResult などを返す場合)、契約内で response セクションに async() メソッドを指定する必要があります。次のコードは例を示しています。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method GET()
        url '/get'
    }
    response {
        status OK()
        body 'Passed'
        async()
    }
}
YAML
response:
    async: true
Java
class contract implements Supplier<Collection<Contract>> {

	@Override
	public Collection<Contract> get() {
		return Collections.singletonList(Contract.make(c -> {
			c.request(r -> {
				// ...
			});
			c.response(r -> {
				r.async();
				// ...
			});
		}));
	}

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        // ...
    }
    response {
        async = true
        // ...
    }
}

fixedDelayMilliseconds メソッドまたはプロパティを使用して、スタブに遅延を追加することもできます。次の例は、その方法を示しています。

Groovy
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method GET()
        url '/get'
    }
    response {
        status 200
        body 'Passed'
        fixedDelayMilliseconds 1000
    }
}
YAML
response:
    fixedDelayMilliseconds: 1000
Java
class contract implements Supplier<Collection<Contract>> {

	@Override
	public Collection<Contract> get() {
		return Collections.singletonList(Contract.make(c -> {
			c.request(r -> {
				// ...
			});
			c.response(r -> {
				r.fixedDelayMilliseconds(1000);
				// ...
			});
		}));
	}

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        // ...
    }
    response {
        delay = fixedMilliseconds(1000)
        // ...
    }
}