2.5. 异步支持
如果在服务器端使用异步通信(您的控制器是 返回,等等),那么,在你的合同中,你必须 在本节中提供方法。以下代码显示了一个示例:Callable
DeferredResult
async()
response
org.springframework.cloud.contract.spec.Contract.make {
request {
method GET()
url '/get'
}
response {
status OK()
body 'Passed'
async()
}
}
还可以使用方法或属性向存根添加延迟。 以下示例演示如何执行此操作:fixedDelayMilliseconds
org.springframework.cloud.contract.spec.Contract.make {
request {
method GET()
url '/get'
}
response {
status 200
body 'Passed'
fixedDelayMilliseconds 1000
}
}
2.6.XML 支持 HTTP
对于 HTTP 协定,我们还支持在请求和响应正文中使用 XML。 XML 主体必须在元素中传递 作为AOR。此外,还可以提供身体匹配器 请求和响应。代替方法,应使用方法,所需提供作为第一个参数 和适当的作为第二个论点。支持除 之外的所有身体匹配器。body
String
GString
jsonPath(…)
org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath
xPath
MatchingType
byType()
以下示例显示了响应正文中带有 XML 的 Groovy DSL 协定:
槽的
亚姆
爪哇岛
科特林
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>customer@test.com</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>
"""
}
}
以下示例显示了在响应正文中自动生成的 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");
}
2.6.1.XML 对命名空间的支持
支持命名空间的 XML。但是,必须更新用于选择命名空间内容的任何 XPath 表达式。
请考虑以下显式命名空间的 XML 文档:
<ns1:customer xmlns:ns1="http://demo.com/customer">
<email>customer@test.com</email>
</ns1:customer>
用于选择电子邮件地址的 XPath 表达式为:。/ns1:customer/email/text()
当心,因为非限定表达式 () 会导致。/customer/email/text() "" |
对于使用非限定命名空间的内容,表达式更详细。请考虑以下 XML 文档, 使用非限定命名空间:
<customer xmlns="http://demo.com/customer">
<email>customer@test.com</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 |
常规命名空间节点表达式语法
/*[local-name=()='<node-name>' and namespace-uri=()='<namespace-uri>']
在某些情况下,您可以省略部分,但这样做可能会导致歧义。namespace_uri |
- 使用非限定命名空间的节点(其祖先之一定义 xmlns 属性):
/*[local-name=()='<node-name>']
2.7. 一个文件中的多个合约
您可以在一个文件中定义多个协定。这样的合同可能类似于 以下示例:
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
name("should post a user")
request {
method 'POST'
url('/users/1')
}
response {
status OK()
}
},
Contract.make {
request {
method 'POST'
url('/users/2')
}
response {
status OK()
}
}
]
在前面的示例中,一个协定具有字段,而另一个协定没有。这 导致生成如下所示的两个测试:name
package org.springframework.cloud.contract.verifier.tests.com.hello;
import com.example.TestBase;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import com.jayway.restassured.response.ResponseOptions;
import org.junit.Test;
import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
public class V1Test extends TestBase {
@Test
public void validate_should_post_a_user() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.post("/users/1");
// then:
assertThat(response.statusCode()).isEqualTo(200);
}
@Test
public void validate_withList_1() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.post("/users/2");
// then:
assertThat(response.statusCode()).isEqualTo(200);
}
}
请注意,对于具有 thefield 的协定,生成的测试方法被命名。没有字段的那个被调用。它对应于文件名和 列表中合同的索引。name
validate_should_post_a_user
name
validate_withList_1
WithList.groovy
生成的存根如以下示例所示:
should post a user.json
1_WithList.json
第一个文件从协定中获取参数。第二个 获取以索引为前缀的合约文件 () 的名称(在此 案例中,合同在文件中的合同列表中有一个索引)。name
WithList.groovy
1
命名您的合同要好得多,因为这样做会使 你的测试更有意义。 |
2.8. 有状态合约
有状态协定(也称为方案)是应读取的协定定义 挨次。这在以下情况下可能很有用:
- 您希望以精确定义的顺序调用合约,因为您使用 Spring 用于测试有状态应用程序的云协定。
我们真的不鼓励你这样做,因为合同测试应该是无状态的。 |
若要创建有状态协定(或方案),需要 在创建协定时使用正确的命名约定。A. 公约 需要包括订单号,后跟下划线。无论这都有效 您是否与 YAML 或 Groovy 合作。下面的清单显示了一个示例:
my_contracts_dir\
scenario1\
1_login.groovy
2_showCart.groovy
3_logout.groovy
这样的树会导致 Spring Cloud 合约验证器生成 WireMock 的场景,其中包含 的名称以及以下三个步骤:scenario1
-
login
,标记为指向...Started
-
showCart
,标记为指向...Step1
-
logout
,标记为(关闭方案)。Step2
您可以在https://wiremock.org/docs/stateful-behaviour/ 找到有关WireMock场景的更多详细信息。
3. 集成
3.1. JAX-RS
Spring Cloud 合约支持 JAX-RS 2 Client API。基类需要 定义和服务器初始化。唯一的选择 测试 JAX-RS API 就是启动一个 Web 服务器。此外,带有正文的请求需要具有 设置内容类型。否则,将使用默认值。protected WebTarget webTarget
application/octet-stream
要使用 JAX-RS 方式,请使用以下设置:
以下示例显示了生成的测试 API:
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Response;
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 javax.ws.rs.client.Entity.*;
@SuppressWarnings("rawtypes")
public class FooTest {
WebTarget webTarget;
@Test
public void validate_() throws Exception {
// when:
Response response = webTarget
.path("/users")
.queryParam("limit", "10")
.queryParam("offset", "20")
.queryParam("filter", "email")
.queryParam("sort", "name")
.queryParam("search", "55")
.queryParam("age", "99")
.queryParam("name", "Denis.Stepanov")
.queryParam("email", "bob@email.com")
.request()
.build("GET")
.invoke();
String responseAsString = response.readEntity(String.class);
// then:
assertThat(response.getStatus()).isEqualTo(200);
// and:
DocumentContext parsedJson = JsonPath.parse(responseAsString);
assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
}
}
3.2. WebFlux with WebTestClient
您可以使用WebTestClient使用WebFlux。以下清单显示了如何 将网络测试客户端配置为测试模式:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>WEBTESTCLIENT</testMode>
</configuration>
</plugin>
下面的示例演示如何设置 WebTestClient 基类和 RestAssured 对于 WebFlux:
import io.restassured.module.webtestclient.RestAssuredWebTestClient;
import org.junit.Before;
public abstract class BeerRestBase {
@Before
public void setup() {
RestAssuredWebTestClient.standaloneSetup(
new ProducerController(personToCheck -> personToCheck.age >= 20));
}
}
}
模式比模式快。WebTestClient EXPLICIT |
3.3. 显式模式的 WebFlux
您还可以在生成的测试中将 WebFlux 与显式模式一起使用 以使用 WebFlux。以下示例演示如何使用显式模式进行配置:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>EXPLICIT</testMode>
</configuration>
</plugin>
下面的示例演示如何为 Web Flux 设置基类和 RestAssured :
@SpringBootTest(classes = BeerRestBase.Config.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "server.port=0")
public abstract class BeerRestBase {
// your tests go here
// in this config class you define all controllers and mocked services
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
PersonCheckingService personCheckingService() {
return personToCheck -> personToCheck.age >= 20;
}
@Bean
ProducerController producerController() {
return new ProducerController(personCheckingService());
}
}
}
3.4. 自定义模式
Spring Cloud 合约允许您提供自己的自定义实现。这样,您就可以使用要发送和接收请求的任何客户端。Spring Cloud Contract 中的默认实现是它使用 OkHttp3 http 客户端。org.springframework.cloud.contract.verifier.http.HttpVerifier
OkHttpHttpVerifier
要开始使用,请设置:testMode
CUSTOM
以下示例显示生成的测试:
package com.example.beer;
import com.example.BeerRestBase;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.http.HttpVerifier;
import org.springframework.cloud.contract.verifier.http.Request;
import org.springframework.cloud.contract.verifier.http.Response;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static org.springframework.cloud.contract.verifier.http.Request.given;
@SuppressWarnings("rawtypes")
public class RestTest extends BeerRestBase {
@Inject HttpVerifier httpVerifier;
@Test
public void validate_shouldGrantABeerIfOldEnough() throws Exception {
// given:
Request request = given()
.post("/beer.BeerService/check")
.scheme("HTTP")
.protocol("h2_prior_knowledge")
.header("Content-Type", "application/grpc")
.header("te", "trailers")
.body(fileToBytes(this, "shouldGrantABeerIfOldEnough_request_PersonToCheck_old_enough.bin"))
.build();
// when:
Response response = httpVerifier.exchange(request);
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/grpc.*");
assertThat(response.header("grpc-encoding")).isEqualTo("identity");
assertThat(response.header("grpc-accept-encoding")).isEqualTo("gzip");
// and:
assertThat(response.getBody().asByteArray()).isEqualTo(fileToBytes(this, "shouldGrantABeerIfOldEnough_response_Response_old_enough.bin"));
}
}
下面的示例演示相应的基类:
@SpringBootTest(classes = BeerRestBase.Config.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BeerRestBase {
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
ProducerController producerController(PersonCheckingService personCheckingService) {
return new ProducerController(personCheckingService);
}
@Bean
PersonCheckingService testPersonCheckingService() {
return argument -> argument.getAge() >= 20;
}
@Bean
HttpVerifier httpOkVerifier(@LocalServerPort int port) {
return new OkHttpHttpVerifier("localhost:" + port);
}
}
}
3.5. 使用上下文路径
春云合约支持上下文路径。
完全支持上下文路径所需的唯一更改是 制片方。此外,自动生成的测试必须使用显式模式。消费者 侧面保持不变。为了使生成的测试通过,必须使用显式 模式。下面的示例演示如何将测试模式设置为:EXPLICIT
马文
格拉德尔
<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>${spring-cloud-contract.version}</version> <extensions>true</extensions> <configuration> <testMode>EXPLICIT</testMode> </configuration> </plugin>
|
这样,您就可以生成不使用 MockMvc 的测试。这意味着您生成 真正的请求,你需要设置你生成的测试的基类来处理一个真实的 插座。
请考虑以下合同:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'GET'
url '/my-context-path/url'
}
response {
status OK()
}
}
下面的示例演示如何设置基类和放心:
import io.restassured.RestAssured;
import org.junit.Before;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ContextPathTestingBaseClass {
@LocalServerPort int port;
@Before
public void setup() {
RestAssured.baseURI = "http://localhost";
RestAssured.port = this.port;
}
}
如果这样做:
- 自动生成的测试中的所有请求都将发送到真实终端节点,其中包含您的 包含上下文路径(例如,)。
/my-context-path/url
- 您的合同反映了您有一个上下文路径。您生成的存根也有 该信息(例如,在存根中,您必须调用)。
/my-context-path/url
3.6. 使用 REST 文档
您可以使用Spring REST 文档生成 文档(例如,Asciidoc格式)用于带有Spring MockMvc的HTTP API, WebTestClient,或RestAssured。在为 API 生成文档的同时,您还可以 通过使用Spring Cloud Contract WireMock生成WireMock存根。为此,请写下您的 正常的 REST 文档测试用例和用于有存根 在 REST 文档输出目录中自动生成。以下 UML 图显示 REST 文档流程:@AutoConfigureRestDocs
以下示例使用:MockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void contextLoads() throws Exception {
mockMvc.perform(get("/resource"))
.andExpect(content().string("Hello World"))
.andDo(document("resource"));
}
}
此测试在 WireMock 存根处生成。它匹配 所有请求路径。与WebTestClient相同的示例(已使用 用于测试 Spring WebFlux 应用程序)将如下所示:target/snippets/stubs/resource.json
GET
/resource
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureWebTestClient
public class ApplicationTests {
@Autowired
private WebTestClient client;
@Test
public void contextLoads() throws Exception {
client.get().uri("/resource").exchange()
.expectBody(String.class).isEqualTo("Hello World")
.consumeWith(document("resource"));
}
}
无需任何其他配置,这些测试将使用请求匹配器创建存根 对于 HTTP 方法和所有标头除外。要匹配 请求更精确(例如,匹配 POST 或 PUT 的主体),我们需要 显式创建请求匹配器。这样做有两个效果:host
content-length
- 创建仅以指定方式匹配的存根。
- 断言测试用例中的请求也匹配相同的条件。
此功能的主要入口点是,可以使用 作为便利方法的替代品,如下 示例显示:WireMockRestDocs.verify()
document()
import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
.andExpect(status().isOk())
.andDo(verify().jsonPath("$.id"))
.andDo(document("resource"));
}
}
前面的协定指定任何带有 anfield 的有效 POST 都会收到响应 在此测试中定义。您可以将调用链接在一起以添加其他 匹配器。如果不熟悉 JSON 路径,JayWay 文档可以帮助您快速上手。此测试的 WebTestClient 版本 有一个类似的静态帮助程序,您可以插入到同一位置。id
.jsonPath()
verify()
除了和方便的方法,您还可以使用 用于验证请求是否与创建的存根匹配的 WireMock API,如 以下示例显示:jsonPath
contentType
@Test
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
.andExpect(status().isOk())
.andDo(verify()
.wiremock(WireMock.post(urlPathEquals("/resource"))
.withRequestBody(matchingJsonPath("$.id"))
.andDo(document("post-resource"))));
}
WireMock API很丰富。您可以通过以下方式匹配标头、查询参数和请求正文 正则表达式以及 JSON 路径。您可以使用这些功能创建具有更宽的存根 参数范围。前面的示例生成类似于以下示例的存根:
post-resource.json
{
"request" : {
"url" : "/resource",
"method" : "POST",
"bodyPatterns" : [ {
"matchesJsonPath" : "$.id"
}]
},
"response" : {
"status" : 200,
"body" : "Hello World",
"headers" : {
"X-Application-Context" : "application:-1",
"Content-Type" : "text/plain"
}
}
}
您可以使用方法或方法创建请求匹配器,但不能同时使用这两种方法。wiremock() jsonPath() contentType() |
在消费者端,您可以在本节前面生成 在类路径上可用(例如,通过将存根发布为 JAR)。之后,您可以在 不同方式的数量,包括使用,如本文前面所述 公文。resource.json
@AutoConfigureWireMock(stubs="classpath:resource.json")
3.6.1. 使用 REST 文档生成合约
您还可以使用 Spring REST 生成 Spring Cloud Contract DSL 文件和文档 文档。如果您与Spring Cloud WireMock结合使用,则可以同时获得两个合同 和存根。
为什么要使用此功能?社区中的一些人提出了问题 关于他们希望迁移到基于DSL的合约定义的情况, 但他们已经有很多Spring MVC测试。使用此功能可以生成 稍后可以修改并移动到文件夹的合同文件(在 配置),以便插件找到它们。
您可能想知道为什么此功能位于 WireMock 模块中。功能 之所以存在,是因为生成合约和存根是有意义的。 |
请考虑以下测试:
this.mockMvc
.perform(post("/foo").accept(MediaType.APPLICATION_PDF).accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON).content("{\"foo\": 23, \"bar\" : \"baz\" }"))
.andExpect(status().isOk()).andExpect(content().string("bar"))
// first WireMock
.andDo(WireMockRestDocs.verify().jsonPath("$[?(@.foo >= 20)]")
.jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]")
.contentType(MediaType.valueOf("application/json")))
// then Contract DSL documentation
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
前面的测试创建上一节中介绍的存根,生成两者 合同和文档文件。
协定被调用,可能类似于以下示例:index.groovy
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method 'POST'
url '/foo'
body('''
{"foo": 23 }
''')
headers {
header('''Accept''', '''application/json''')
header('''Content-Type''', '''application/json''')
}
}
response {
status OK()
body('''
bar
''')
headers {
header('''Content-Type''', '''application/json;charset=UTF-8''')
header('''Content-Length''', '''3''')
}
bodyMatchers {
jsonPath('$[?(@.foo >= 20)]', byType())
}
}
}
生成的文档(在本例中为 Asciidoc 格式)包含格式化的 合同。此文件的位置将是。index/dsl-contract.adoc
3.7. 图形QL
由于GraphQL本质上是 HTTP,您可以通过创建一个标准 HTTP 合约来为其编写合约,该合约带有一个带有 keyand a 映射的附加条目。metadata
verifier
tool=graphql
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method(POST())
url("/graphql")
headers {
contentType("application/json")
}
body('''
{
"query":"query queryName($personName: String!) {\\n personToCheck(name: $personName) {\\n name\\n age\\n }\\n}\\n\\n\\n\\n",
"variables":{"personName":"Old Enough"},
"operationName":"queryName"
}
''')
}
response {
status(200)
headers {
contentType("application/json")
}
body('''\
{
"data": {
"personToCheck": {
"name": "Old Enough",
"age": "40"
}
}
}
''')
}
metadata(verifier: [
tool: "graphql"
])
}
添加元数据部分将更改默认的 WireMock 存根的构建方式。它现在将使用 Spring Cloud 合约请求匹配器,例如,通过忽略空格将 GraphQL 请求的一部分与真实请求进行比较。query
3.7.1. 生产者端设置
在生产者端,您的配置可以如下所示。
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>EXPLICIT</testMode>
<baseClassForTests>com.example.BaseClass</baseClassForTests>
</configuration>
</plugin>
基类将设置在随机端口上运行的应用程序。
基类
@SpringBootTest(classes = ProducerApplication.class,
properties = "graphql.servlet.websocket.enabled=false",
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BaseClass {
@LocalServerPort int port;
@BeforeEach
public void setup() {
RestAssured.baseURI = "http://localhost:" + port;
}
}
3.7.2. 消费者端设置
GraphQL API 的消费者端测试示例。
消费者侧测试
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class BeerControllerGraphQLTest {
@RegisterExtension
static StubRunnerExtension rule = new StubRunnerExtension()
.downloadStub("com.example","beer-api-producer-graphql")
.stubsMode(StubRunnerProperties.StubsMode.LOCAL);
private static final String REQUEST_BODY = "{\n"
+ "\"query\":\"query queryName($personName: String!) {\\n personToCheck(name: $personName) {\\n name\\n age\\n }\\n}\","
+ "\"variables\":{\"personName\":\"Old Enough\"},\n"
+ "\"operationName\":\"queryName\"\n"
+ "}";
@Test
public void should_send_a_graphql_request() {
ResponseEntity<String> responseEntity = new RestTemplate()
.exchange(RequestEntity
.post(URI.create("http://localhost:" + rule.findStubUrl("beer-api-producer-graphql").getPort() + "/graphql"))
.contentType(MediaType.APPLICATION_JSON)
.body(REQUEST_BODY), String.class);
BDDAssertions.then(responseEntity.getStatusCodeValue()).isEqualTo(200);
}
}
3.8. GRPC
GRPC是一个建立在HTTP / 2之上的RPC框架,Spring Cloud Contract对此有基本的支持。
Spring Cloud Contract 对 GRPC 的基本用例提供了实验性支持。不幸的是,由于GRPC对HTTP / 2标头帧的调整,无法断言标头。grpc-status |
让我们看一下下面的合约。
时髦的合同
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.http.ContractVerifierHttpMetaData
Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/beer.BeerService/check'
body(fileAsBytes("PersonToCheck_old_enough.bin"))
headers {
contentType("application/grpc")
header("te", "trailers")
}
}
response {
status 200
body(fileAsBytes("Response_old_enough.bin"))
headers {
contentType("application/grpc")
header("grpc-encoding", "identity")
header("grpc-accept-encoding", "gzip")
}
}
metadata([
"verifierHttp": [
"protocol": ContractVerifierHttpMetaData.Protocol.H2_PRIOR_KNOWLEDGE.toString()
]
])
}
3.8.1. 生产者端设置
为了利用HTTP / 2支持,您必须按如下方式设置测试模式。CUSTOM
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testMode>CUSTOM</testMode>
<packageWithBaseClasses>com.example</packageWithBaseClasses>
</configuration>
</plugin>
基类将设置在随机端口上运行的应用程序。它还会将实现设置为可以使用HTTP / 2协议的实现。春云合约随实施而来。HttpVerifier
OkHttpHttpVerifier
基类
@SpringBootTest(classes = BeerRestBase.Config.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {
"grpc.server.port=0"
})
public abstract class BeerRestBase {
@Autowired
GrpcServerProperties properties;
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
ProducerController producerController(PersonCheckingService personCheckingService) {
return new ProducerController(personCheckingService);
}
@Bean
PersonCheckingService testPersonCheckingService() {
return argument -> argument.getAge() >= 20;
}
@Bean
HttpVerifier httpOkVerifier(GrpcServerProperties properties) {
return new OkHttpHttpVerifier("localhost:" + properties.getPort());
}
}
}
3.8.2. 消费者端设置
GRPC消费者侧测试示例。由于 GRPC 服务器端的异常行为,存根无法在适当的时刻返回标头。这就是为什么我们需要手动设置返回状态的原因。grpc-status
消费者侧测试
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = GrpcTests.TestConfiguration.class, properties = {
"grpc.client.beerService.address=static://localhost:5432", "grpc.client.beerService.negotiatinotallow=TLS"
})
public class GrpcTests {
@GrpcClient(value = "beerService", interceptorNames = "fixedStatusSendingClientInterceptor")
BeerServiceGrpc.BeerServiceBlockingStub beerServiceBlockingStub;
int port;
@RegisterExtension
static StubRunnerExtension rule = new StubRunnerExtension()
.downloadStub("com.example", "beer-api-producer-grpc")
// With WireMock PlainText mode you can just set an HTTP port
// .withPort(5432)
.stubsMode(StubRunnerProperties.StubsMode.LOCAL)
.withHttpServerStubConfigurer(MyWireMockConfigurer.class);
@BeforeEach
public void setupPort() {
this.port = rule.findStubUrl("beer-api-producer-grpc").getPort();
}
@Test
public void should_give_me_a_beer_when_im_old_enough() throws Exception {
Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(23).build());
BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.OK);
}
@Test
public void should_reject_a_beer_when_im_too_young() throws Exception {
Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(17).build());
response = response == null ? Response.newBuilder().build() : response;
BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.NOT_OK);
}
// Not necessary with WireMock PlainText mode
static class MyWireMockConfigurer extends WireMockHttpServerStubConfigurer {
@Override
public WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
return httpStubConfiguration
.httpsPort(5432);
}
}
@Configuration
@ImportAutoConfiguration(GrpcClientAutoConfiguration.class)
static class TestConfiguration {
// Not necessary with WireMock PlainText mode
@Bean
public GrpcChannelConfigurer keepAliveClientConfigurer() {
return (channelBuilder, name) -> {
if (channelBuilder instanceof NettyChannelBuilder) {
try {
((NettyChannelBuilder) channelBuilder)
.sslContext(GrpcSslContexts.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build());
}
catch (SSLException e) {
throw new IllegalStateException(e);
}
}
};
}
/**
* GRPC client interceptor that sets the returned status always to OK.
* You might want to change the return status depending on the received stub payload.
*
* Hopefully in the future this will be unnecessary and will be removed.
*/
@Bean
ClientInterceptor fixedStatusSendingClientInterceptor() {
return new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
return new ClientCall<ReqT, RespT>() {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
Listener<RespT> listener = new Listener<RespT>() {
@Override
public void onHeaders(Metadata headers) {
responseListener.onHeaders(headers);
}
@Override
public void onMessage(RespT message) {
responseListener.onMessage(message);
}
@Override
public void onClose(Status status, Metadata trailers) {
// TODO: This must be fixed somehow either in Jetty (WireMock) or somewhere else
responseListener.onClose(Status.OK, trailers);
}
@Override
public void onReady() {
responseListener.onReady();
}
};
call.start(listener, headers);
}
@Override
public void request(int numMessages) {
call.request(numMessages);
}
@Override
public void cancel(@Nullable String message, @Nullable Throwable cause) {
call.cancel(message, cause);
}
@Override
public void halfClose() {
call.halfClose();
}
@Override
public void sendMessage(ReqT message) {
call.sendMessage(message);
}
};
}
};
}
}
}
4. 消息传递
Spring 云合同允许您验证使用消息传递的应用程序 通讯手段。本文档中显示的所有集成都适用于 Spring, 但您也可以创建自己的一个并使用它。
4.1. 消息传递 DSL *元素
消息传递的DSL看起来与专注于HTTP的DSL略有不同。这 以下各节解释了这些差异:
- 方法触发的输出
- 消息触发的输出
- 消费者/生产者
- 常见
4.1.1. 方法触发的输出
输出消息可以通过调用方法触发(例如当合约 已启动和发送消息的时间),如以下示例所示:Scheduler
def dsl = Contract.make {
// Human readable description
description 'Some description'
// Label by means of which the output message can be triggered
label 'some_label'
// input to the contract
input {
// the contract will be triggered by a method
triggeredBy('bookReturnedTriggered()')
}
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo('output')
// the body of the output message
body('''{ "bookName" : "foo" }''')
// the headers of the output message
headers {
header('BOOK-NAME', 'foo')
}
}
}
在前面的示例示例中,如果调用了调用的方法,则输出消息将发送到。在消息发布者方面,我们生成一个 测试调用该方法以触发消息。在使用者端,您可以使用 触发消息。output
bookReturnedTriggered
some_label
4.1.2. 消息触发的输出
通过接收消息可以触发输出消息,如下所示 例:
def dsl = Contract.make {
description 'Some Description'
label 'some_label'
// input is a message
input {
// the message was received from this destination
messageFrom('input')
// has the following body
messageBody([
bookName: 'foo'
])
// and the following headers
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
在前面的示例中,如果正确的消息是 在目的地收到。在消息发布者方面,引擎 生成将输入消息发送到定义的目标的测试。在 消费者端,可以将消息发送到输入目标或使用标签 (在示例中)以触发消息。output
input
some_label
4.1.3. 消费者/生产者
在HTTP中,你有一个//notation的概念。你也可以 在消息传递中使用这些范例。此外,春云合约验证器还 提供与方法,如以下示例所示 (请注意,您可以使用 CanorMethods 来提供和部分):client
stub and `server
test
consumer
producer
$
value
consumer
producer
Contract.make {
name "foo"
label 'some_label'
input {
messageFrom value(consumer('jms:output'), producer('jms:input'))
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo $(consumer('jms:input'), producer('jms:output'))
body([
bookName: 'foo'
])
}
}
4.1.4. 常见
在理论部分中,您可以调用名称 您在 基类或静态导入。春云合约运行该方法 在生成的测试中。input
outputMessage
assertThat
method
assertThatMessageIsOnTheQueue()
4.2. 集成
您可以使用以下四种集成配置之一:
- 阿帕奇骆驼
- 弹簧集成
- 春云溪
- 春季AMQP
- Spring JMS(需要嵌入式代理)
- Spring Kafka (需要嵌入式代理)
由于我们使用 Spring Boot,如果您已将这些库之一添加到类路径中,则所有 消息传递配置是自动设置的。
记得把你的基类 生成的测试。否则,春云合约的消息部分不会 工作。@AutoConfigureMessageVerifier |
如果要使用 Spring Cloud Stream,请记住添加测试依赖项,如下所示:org.springframework.cloud:spring-cloud-stream
马文
格拉德尔
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream</artifactId> <type>test-jar</type> <scope>test</scope> <classifier>test-binder</classifier> </dependency>
|
4.2.1. 手动集成测试
测试使用的主界面是。 它定义了如何发送和接收消息。您可以创建自己的实现来 实现相同的目标。org.springframework.cloud.contract.verifier.messaging.MessageVerifier
在测试中,您可以注入 ato 发送和接收 合同后面的消息。然后添加到您的测试中。 以下示例演示如何执行此操作:ContractVerifierMessageExchange
@AutoConfigureMessageVerifier
@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureMessageVerifier
public static class MessagingContractTests {
@Autowired
private MessageVerifier verifier;
...
}
如果您的测试也需要存根,则包括 消息传递配置,因此您只需要一个注释。@AutoConfigureStubRunner |
4.3. 生产者端消息传递测试生成
在DSL中使用理论部分可以创建测试 在出版商方面。默认情况下,将创建 JUnit 4 测试。但是,还有一个 可以创建JUnit 5,TestNG或Spock测试。input
outputMessage
我们应该考虑三种主要情况:
- 场景 1:没有生成输出消息的输入消息。输出 消息由应用程序内部的组件(例如,计划程序)触发。
- 场景 2:输入消息触发输出消息。
- 场景三:输入消息被消费,没有输出消息。
通过的目的地有不同的 不同消息传递实现的含义。对于流和集成,它是 首先解析为 AOF 通道。然后,如果没有这样的, 它解析为通道名称。对于骆驼来说,这是一个特定的组件(例如,)。messageFrom sentTo destination destination jms |
4.3.1. 场景 1:无输入消息
请考虑以下合同:
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}
对于前面的示例,将创建以下测试:
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
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 org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// when:
bookReturnedTriggered();
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
contract(this, "foo.yml"));
assertThat(response).isNotNull();
// and:
assertThat(response.getHeader("BOOK-NAME")).isNotNull();
assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
assertThat(response.getHeader("contentType")).isNotNull();
assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
}
}
4.3.2. 场景 2:输入触发输出
请考虑以下合同:
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
对于前面的协定,将创建以下测试:
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
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 org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header")
);
// when:
contractVerifierMessaging.send(inputMessage, "jms:input",
contract(this, "foo.yml"));
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
contract(this, "foo.yml"));
assertThat(response).isNotNull();
// and:
assertThat(response.getHeader("BOOK-NAME")).isNotNull();
assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
}
}
4.3.3. 场景 3:无输出消息
请考虑以下合同:
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
对于前面的协定,将创建以下测试:
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
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 org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header")
);
// when:
contractVerifierMessaging.send(inputMessage, "jms:delete",
contract(this, "foo.yml"));
bookWasDeleted();
}
}