Spring Cloud 合约功能
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud 合约功能相关的知识,希望对你有一定的参考价值。
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("[\\\\pL]*");
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()
当心,因为非限定表达式 () 会导致。 |
对于使用非限定命名空间的内容,表达式更详细。请考虑以下 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()
当心,作为非限定表达式(或) 导致。甚至子元素也必须用语法引用。 |
常规命名空间节点表达式语法
- 使用限定命名空间的节点:
/<node-name>
- 使用和定义非限定命名空间的节点:
/*[local-name=()=<node-name> and 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 方式,请使用以下设置:
testMode = JAXRSCLIENT
以下示例显示了生成的测试 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));
模式比模式快。 |
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
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. 使用上下文路径
春云合约支持上下文路径。
完全支持上下文路径所需的唯一更改是 制片方。此外,自动生成的测试必须使用显式模式。消费者 侧面保持不变。为了使生成的测试通过,必须使用显式 模式。下面的示例演示如何将测试模式设置为: 马文 格拉德尔 <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"
您可以使用方法或方法创建请求匹配器,但不能同时使用这两种方法。 |
在消费者端,您可以在本节前面生成 在类路径上可用(例如,通过将存根发布为 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标头帧的调整,无法断言标头。 |
让我们看一下下面的合约。
时髦的合同
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:
well 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()
.downl以上是关于Spring Cloud 合约功能的主要内容,如果未能解决你的问题,请参考以下文章
spring cloud 合约 - Feign Clients
如何在 spring-cloud-gateway 合约测试中从 spring-cloud-contract 中设置带有 StubRunner 端口的 url