Spring Cloud 合约功能

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud 合约功能相关的知识,希望对你有一定的参考价值。

Spring

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()​

当心,因为非限定表达式 () 会导致。​​/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​

常规命名空间节点表达式语法
  • 使用限定命名空间的节点:

/<node-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​

  1. ​login​​​,标记为指向...​​Started​
  2. ​showCart​​​,标记为指向...​​Step1​
  3. ​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));


模式比模式快。​​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​

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​

Spring

以下示例使用:​​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:
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 合约功能

使用Spring Cloud合约进行消费者驱动的合同测试

如何在 spring-cloud-gateway 合约测试中从 spring-cloud-contract 中设置带有 StubRunner 端口的 url

在同一个项目中配置spring cloud合约和zuul代理

在非 Spring 项目中运行 Spring Cloud Contract 测试