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 标签或方法传递一个较小的数字。例如,值为 5priority 比值为 10priority 具有更高的优先级。

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

request 可以包含查询参数,如下例(使用 urlPath)所示

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 元素。要包含 multipart 元素,请使用 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
    }
}

在前面的示例中,我们通过以下两种方式之一定义了参数

编码 DSL
  • 直接使用 map 符号,其中值可以是动态属性(例如 formParameter: $(consumer(…​), producer(…​)))。

  • 使用 named(…​) 方法,该方法允许您设置一个命名参数。命名参数可以设置 namecontent。您可以通过使用带有两个参数的方法调用它,例如 named("fileName", "fileContent"),或者使用 map 符号,例如 named(name: "fileName", content: "fileContent")

YAML
  • Multipart 参数在 multipart.params 节中设置。

  • 命名参数(给定参数名称的 fileNamefileContent)可以在 multipart.named 节中设置。该节包含 paramName(参数名称)、fileName(文件名)、fileContent(文件内容)字段。

  • 动态部分可以在 matchers.multipart 节中设置。

    • 对于参数,使用 params 节,该节可以接受 regexpredefined 正则表达式。

    • 对于命名参数,使用 named 节,首先使用 paramName 定义参数名称。然后可以在 regexpredefined 正则表达式中传递 fileNamefileContent 的参数化。

对于 named(…​) 节,您始终必须添加一对 value(producer(…​), consumer(…​)) 调用。仅设置 DSL 属性,例如只设置 value(producer(…​)) 或只设置 file(…​) 将不起作用。查看此问题以获取更多信息。

根据前面示例中的契约,生成的测试和存根如下所示

测试
// 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);
存根
			'''
{
  "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 体必须作为 StringGString 传递到 body 元素中。此外,可以为请求和响应提供体匹配器。代替 jsonPath(…​) 方法,应使用 org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath 方法,将期望的 xPath 作为第一个参数提供,将适当的 MatchingType 作为第二个参数提供。除了 byType() 之外,所有体匹配器都受支持。

以下示例展示了一个 Groovy DSL 契约,其响应体中包含 XML

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()
*[local-name()='customer' and namespace-uri()='http://demo.com/customer']/*[local-name()='email' and namespace-uri()='http://demo.com/customer']/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 部分,但这可能导致歧义。

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

使用未限定命名空间的节点(其某个祖先定义了 xmlns 属性)

异步支持

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

如果您在服务器端使用异步通信(您的控制器返回 CallableDeferredResult 等),那么在您的契约中,您必须在 response 节中提供一个 async() 方法。以下代码显示了一个示例

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