Split files
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
[[features-context-paths]]
|
||||
= Working with Context Paths
|
||||
|
||||
Spring Cloud Contract supports context paths.
|
||||
|
||||
[IMPORTANT]
|
||||
=====
|
||||
The only change needed to fully support context paths is the switch on the
|
||||
producer side. Also, the autogenerated tests must use explicit mode. The consumer
|
||||
side remains untouched. In order for the generated test to pass, you must use explicit
|
||||
mode. The following example shows how to set the test mode to `EXPLICIT`:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<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>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
contracts {
|
||||
testMode = 'EXPLICIT'
|
||||
}
|
||||
----
|
||||
====
|
||||
=====
|
||||
|
||||
That way, you generate a test that does not use MockMvc. It means that you generate
|
||||
real requests and you need to set up your generated test's base class to work on a real
|
||||
socket.
|
||||
|
||||
Consider the following contract:
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
include:../:{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy[tags=context_path_contract,indent=0]
|
||||
----
|
||||
|
||||
The following example shows how to set up a base class and RestAssured:
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
include:../:{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy[tags=context_path_baseclass,indent=0]
|
||||
----
|
||||
|
||||
If you do it this way:
|
||||
|
||||
* All of your requests in the autogenerated tests are sent to the real endpoint with your
|
||||
context path included (for example, `/my-context-path/url`).
|
||||
* Your contracts reflect that you have a context path. Your generated stubs also have
|
||||
that information (for example, in the stubs, you have to call `/my-context-path/url`).
|
||||
|
||||
104
docs/modules/ROOT/pages/_project-features-flows/custom-mode.adoc
Normal file
104
docs/modules/ROOT/pages/_project-features-flows/custom-mode.adoc
Normal file
@@ -0,0 +1,104 @@
|
||||
[[features-custom-mode]]
|
||||
= Custom Mode
|
||||
|
||||
IMPORTANT: This mode is experimental and can change in the future.
|
||||
|
||||
The Spring Cloud Contract lets you provide your own, custom, implementation of the
|
||||
`org.springframework.cloud.contract.verifier.http.HttpVerifier`. That way, you can use any client you want to send and receive a request. The default implementation in Spring Cloud Contract is `OkHttpHttpVerifier` and it uses OkHttp3 http client.
|
||||
|
||||
To get started, set `testMode` to `CUSTOM`:
|
||||
|
||||
====
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
testMode = 'CUSTOM'
|
||||
----
|
||||
====
|
||||
|
||||
The following example shows a generated test:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
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"));
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The following example shows a corresponding base class:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
[[feature-webflux-explicit]]
|
||||
= WebFlux with Explicit Mode
|
||||
|
||||
You can also use WebFlux with the explicit mode in your generated tests
|
||||
to work with WebFlux. The following example shows how to configure using explicit mode:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<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>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
contracts {
|
||||
testMode = 'EXPLICIT'
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The following example shows how to set up a base class and RestAssured for Web Flux:
|
||||
|
||||
====
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
include:../:{samples_url}/producer_webflux/src/test/java/com/example/BeerRestBase.java[tags=annotations,indent=0]
|
||||
|
||||
// your tests go here
|
||||
|
||||
// in this config class you define all controllers and mocked services
|
||||
include:../:{samples_url}/producer_webflux/src/test/java/com/example/BeerRestBase.java[tags=config,indent=0]
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
[[feature-webflux]]
|
||||
= WebFlux with WebTestClient
|
||||
|
||||
You can work with WebFlux by using WebTestClient. The following listing shows how to
|
||||
configure WebTestClient as the test mode:
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<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>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
contracts {
|
||||
testMode = 'WEBTESTCLIENT'
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The following example shows how to set up a WebTestClient base class and RestAssured
|
||||
for WebFlux:
|
||||
|
||||
====
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
TIP: The `WebTestClient` mode is faster than the `EXPLICIT` mode.
|
||||
|
||||
185
docs/modules/ROOT/pages/_project-features-flows/graphql.adoc
Normal file
185
docs/modules/ROOT/pages/_project-features-flows/graphql.adoc
Normal file
@@ -0,0 +1,185 @@
|
||||
[[features-graphql]]
|
||||
= GraphQL
|
||||
|
||||
Since https://graphql.org/[GraphQL] is essentially HTTP you can write a contract for it by creating a standard HTTP contract with an additional `metadata` entry with key `verifier` and a mapping `tool=graphql`.
|
||||
|
||||
====
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
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"
|
||||
])
|
||||
}
|
||||
----
|
||||
|
||||
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.YAML
|
||||
----
|
||||
---
|
||||
request:
|
||||
method: "POST"
|
||||
url: "/graphql"
|
||||
headers:
|
||||
Content-Type: "application/json"
|
||||
body:
|
||||
query: "query queryName($personName: String!) { personToCheck(name: $personName)
|
||||
{ name age } }"
|
||||
variables:
|
||||
personName: "Old Enough"
|
||||
operationName: "queryName"
|
||||
matchers:
|
||||
headers:
|
||||
- key: "Content-Type"
|
||||
regex: "application/json.*"
|
||||
regexType: "as_string"
|
||||
response:
|
||||
status: 200
|
||||
headers:
|
||||
Content-Type: "application/json"
|
||||
body:
|
||||
data:
|
||||
personToCheck:
|
||||
name: "Old Enough"
|
||||
age: "40"
|
||||
matchers:
|
||||
headers:
|
||||
- key: "Content-Type"
|
||||
regex: "application/json.*"
|
||||
regexType: "as_string"
|
||||
name: "shouldRetrieveOldEnoughPerson"
|
||||
metadata:
|
||||
verifier:
|
||||
tool: "graphql"
|
||||
----
|
||||
====
|
||||
|
||||
Adding the metadata section will change the way the default, WireMock stub is built. It will now use the Spring Cloud Contract request matcher, so that e.g. the `query` part of the GraphQL request gets compared against the real request by ignoring whitespaces.
|
||||
|
||||
[[features-graphql-producer]]
|
||||
== Producer Side Setup
|
||||
|
||||
On the producer side your configuration can look as follows.
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<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>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
contracts {
|
||||
testMode = "EXPLICIT"
|
||||
baseClassForTests = "com.example.BaseClass"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The base class would set up the application running on a random port.
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes"]
|
||||
.Base Class
|
||||
----
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
====
|
||||
|
||||
[[features-graphql-consumer]]
|
||||
== Consumer Side Setup
|
||||
|
||||
Example of a consumer side test of the GraphQL API.
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes"]
|
||||
.Consumer Side Test
|
||||
----
|
||||
@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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
====
|
||||
|
||||
277
docs/modules/ROOT/pages/_project-features-flows/grpc.adoc
Normal file
277
docs/modules/ROOT/pages/_project-features-flows/grpc.adoc
Normal file
@@ -0,0 +1,277 @@
|
||||
[[features-grpc]]
|
||||
= GRPC
|
||||
|
||||
https://grpc.io/[GRPC] is an RPC framework built on top of HTTP/2 for which Spring Cloud Contract has basic support.
|
||||
|
||||
IMPORTANT: Spring Cloud Contract has an experimental support for basic use cases of GRPC. Unfortunately, due to GRPC's tweaking of the HTTP/2 Header frames, it's impossible to assert the `grpc-status` header.
|
||||
|
||||
Let's look at the following contract.
|
||||
|
||||
====
|
||||
[source,groovy,indent=0,subs="verbatim,attributes"]
|
||||
.Groovy contract
|
||||
----
|
||||
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()
|
||||
]
|
||||
])
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[features-grpc-producer]]
|
||||
== Producer Side Setup
|
||||
|
||||
In order to leverage the HTTP/2 support you must set the `CUSTOM` test mode as follow.
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<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>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
contracts {
|
||||
packageWithBaseClasses = 'com.example'
|
||||
testMode = "CUSTOM"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The base class would set up the application running on a random port. It will also set the `HttpVerifier` implementation to one that can use the HTTP/2 protocol. Spring Cloud Contract comes with the `OkHttpHttpVerifier` implementation.
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes"]
|
||||
.Base Class
|
||||
----
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[features-grpc-consumer]]
|
||||
== Consumer Side Setup
|
||||
|
||||
Example of GRPC consumer side test. Due to the unusual behaviour of the GRPC server side, the stub is unable to return the `grpc-status` header in the proper moment. This is why we need to manually set the return status.
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes"]
|
||||
.Consumer Side Test
|
||||
----
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = GrpcTests.TestConfiguration.class, properties = {
|
||||
"grpc.client.beerService.address=static://localhost:5432", "grpc.client.beerService.negotiationType=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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
====
|
||||
26
docs/modules/ROOT/pages/_project-features-flows/jax-rs.adoc
Normal file
26
docs/modules/ROOT/pages/_project-features-flows/jax-rs.adoc
Normal file
@@ -0,0 +1,26 @@
|
||||
[[features-jax-rs]]
|
||||
= JAX-RS
|
||||
|
||||
The Spring Cloud Contract supports the JAX-RS 2 Client API. The base class needs
|
||||
to define `protected WebTarget webTarget` and server initialization. The only option for
|
||||
testing JAX-RS API is to start a web server. Also, a request with a body needs to have a
|
||||
content type be set. Otherwise, the default of `application/octet-stream` gets used.
|
||||
|
||||
To use JAX-RS mode, use the following setting:
|
||||
|
||||
====
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
testMode = 'JAXRSCLIENT'
|
||||
----
|
||||
====
|
||||
|
||||
The following example shows a generated test API:
|
||||
|
||||
====
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
include:../:{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/JaxRsClientMethodBuilderSpec.groovy[tags=jaxrs,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
307
docs/modules/ROOT/pages/_project-features-flows/rest-docs.adoc
Normal file
307
docs/modules/ROOT/pages/_project-features-flows/rest-docs.adoc
Normal file
@@ -0,0 +1,307 @@
|
||||
[[features-rest-docs]]
|
||||
= Working with REST Docs
|
||||
|
||||
You can use https://projects.spring.io/spring-restdocs[Spring REST Docs] to generate
|
||||
documentation (for example, in Asciidoc format) for an HTTP API with Spring MockMvc,
|
||||
WebTestClient, or RestAssured. At the same time that you generate documentation for your API, you can also
|
||||
generate WireMock stubs by using Spring Cloud Contract WireMock. To do so, write your
|
||||
normal REST Docs test cases and use `@AutoConfigureRestDocs` to have stubs be
|
||||
automatically generated in the REST Docs output directory. The following UML diagram shows
|
||||
the REST Docs flow:
|
||||
|
||||
[plantuml, rest-docs, png]
|
||||
----
|
||||
"API Producer"->"API Producer": Add Spring Cloud Contract (SCC) \nStub Runner dependency
|
||||
"API Producer"->"API Producer": Set up stub jar assembly
|
||||
"API Producer"->"API Producer": Write and set up REST Docs tests
|
||||
"API Producer"->"Build": Run build
|
||||
"Build"->"REST Docs": Generate API \ndocumentation
|
||||
"REST Docs"->"SCC": Generate stubs from the \nREST Docs tests
|
||||
"REST Docs"->"SCC": Generate contracts from the \nREST Docs tests
|
||||
"Build"->"Build": Assemble stubs jar with \nstubs and contracts
|
||||
"Build"->"Nexus / Artifactory": Upload contracts \nand stubs and the project arifact
|
||||
"Build"->"API Producer": Build successful
|
||||
"API Consumer"->"API Consumer": Add SCC Stub Runner \ndependency
|
||||
"API Consumer"->"API Consumer": Write a SCC Stub Runner \nbased contract test
|
||||
"SCC Stub Runner"->"Nexus / Artifactory": Test asks for [API Producer] stubs
|
||||
"Nexus / Artifactory"->"SCC Stub Runner": Fetch the [API Producer] stubs
|
||||
"SCC Stub Runner"->"SCC Stub Runner": Run in memory\n HTTP server stubs
|
||||
"API Consumer"->"SCC Stub Runner": Send a request \nto the HTTP server stub
|
||||
"SCC Stub Runner"->"API Consumer": Communication is correct
|
||||
----
|
||||
|
||||
The following example uses `MockMvc`:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@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"));
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This test generates a WireMock stub at `target/snippets/stubs/resource.json`. It matches
|
||||
all `GET` requests to the `/resource` path. The same example with WebTestClient (used
|
||||
for testing Spring WebFlux applications) would be as follows:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@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"));
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Without any additional configuration, these tests create a stub with a request matcher
|
||||
for the HTTP method and all headers except `host` and `content-length`. To match the
|
||||
request more precisely (for example, to match the body of a POST or PUT), we need to
|
||||
explicitly create a request matcher. Doing so has two effects:
|
||||
|
||||
* Creating a stub that matches only in the way you specify.
|
||||
* Asserting that the request in the test case also matches the same conditions.
|
||||
|
||||
The main entry point for this feature is `WireMockRestDocs.verify()`, which can be used
|
||||
as a substitute for the `document()` convenience method, as the following
|
||||
example shows:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
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"));
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The preceding contract specifies that any valid POST with an `id` field receives the response
|
||||
defined in this test. You can chain together calls to `.jsonPath()` to add additional
|
||||
matchers. If JSON Path is unfamiliar, the https://github.com/jayway/JsonPath[JayWay
|
||||
documentation] can help you get up to speed. The WebTestClient version of this test
|
||||
has a similar `verify()` static helper that you insert in the same place.
|
||||
|
||||
Instead of the `jsonPath` and `contentType` convenience methods, you can also use the
|
||||
WireMock APIs to verify that the request matches the created stub, as the
|
||||
following example shows:
|
||||
|
||||
====
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@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"))));
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The WireMock API is rich. You can match headers, query parameters, and the request body by
|
||||
regex as well as by JSON path. You can use these features to create stubs with a wider
|
||||
range of parameters. The preceding example generates a stub resembling the following example:
|
||||
|
||||
====
|
||||
.post-resource.json
|
||||
[source,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: You can use either the `wiremock()` method or the `jsonPath()` and `contentType()`
|
||||
methods to create request matchers, but you cannot use both approaches.
|
||||
|
||||
On the consumer side, you can make the `resource.json` generated earlier in this section
|
||||
available on the classpath (by
|
||||
<<features-stub-runner-publishing-stubs-as-jars>>, for example). After that, you can create a stub that uses WireMock in a
|
||||
number of different ways, including by using
|
||||
`@AutoConfigureWireMock(stubs="classpath:resource.json")`, as described earlier in this
|
||||
document.
|
||||
|
||||
[[features-rest-docs-contracts]]
|
||||
== Generating Contracts with REST Docs
|
||||
|
||||
You can also generate Spring Cloud Contract DSL files and documentation with Spring REST
|
||||
Docs. If you do so in combination with Spring Cloud WireMock, you get both the contracts
|
||||
and the stubs.
|
||||
|
||||
Why would you want to use this feature? Some people in the community asked questions
|
||||
about a situation in which they would like to move to DSL-based contract definition,
|
||||
but they already have a lot of Spring MVC tests. Using this feature lets you generate
|
||||
the contract files that you can later modify and move to folders (defined in your
|
||||
configuration) so that the plugin finds them.
|
||||
|
||||
NOTE: You might wonder why this functionality is in the WireMock module. The functionality
|
||||
is there because it makes sense to generate both the contracts and the stubs.
|
||||
|
||||
Consider the following test:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include:../:{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/restdocs/ContractDslSnippetTests.java[tags=contract_snippet]
|
||||
----
|
||||
====
|
||||
|
||||
The preceding test creates the stub presented in the previous section, generating both
|
||||
the contract and a documentation file.
|
||||
|
||||
The contract is called `index.groovy` and might resemble the following example:
|
||||
|
||||
====
|
||||
[source,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The generated document (formatted in Asciidoc in this case) contains a formatted
|
||||
contract. The location of this file would be `index/dsl-contract.adoc`.
|
||||
|
||||
[[features-restdocs-priority-attribute]]
|
||||
== Specifying the priority attribute
|
||||
|
||||
The method `SpringCloudContractRestDocs.dslContract()` takes an optional Map parameter that allows you to specify additional attributes in the template.
|
||||
|
||||
One of these attributes is the <<contract-dsl-http-top-level-elements,priority>> field that you may specify as follows:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
SpringCloudContractRestDocs.dslContract(Map.of("priority", 1))
|
||||
----
|
||||
|
||||
[[features-restdocs-override]]
|
||||
== Overriding the DSL contract template
|
||||
|
||||
By default, the output of the contract is based on a file named `default-dsl-contract-only.snippet`.
|
||||
|
||||
You may provide a custom template file instead by overriding the getTemplate() method as follows:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
new ContractDslSnippet(){
|
||||
@Override
|
||||
protected String getTemplate() {
|
||||
return "custom-dsl-contract";
|
||||
}
|
||||
}));
|
||||
----
|
||||
|
||||
so the example above showing this line
|
||||
[source,java,indent=0]
|
||||
----
|
||||
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
|
||||
----
|
||||
|
||||
should be changed to:
|
||||
[source,java,indent=0]
|
||||
----
|
||||
.andDo(document("index", new ContractDslSnippet(){
|
||||
@Override
|
||||
protected String getTemplate() {
|
||||
return "custom-dsl-template";
|
||||
}
|
||||
}));
|
||||
----
|
||||
|
||||
Templates are resolved by looking for resources on the classpath. The following locations are checked in order:
|
||||
|
||||
* `org/springframework/restdocs/templates/${templateFormatId}/${name}.snippet`
|
||||
* `org/springframework/restdocs/templates/${name}.snippet`
|
||||
* `org/springframework/restdocs/templates/${templateFormatId}/default-${name}.snippet`
|
||||
|
||||
Therefore in the example above you should place a file named custom-dsl-template.snippet in `src/test/resources/org/springframework/restdocs/templates/custom-dsl-template.snippet`
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user