Custom mode & GRPC support
This commit is contained in:
committed by
Marcin Grzejszczak
parent
8892c03ae8
commit
33c7219336
@@ -127,6 +127,108 @@ include::{samples_url}/producer_webflux/src/test/java/com/example/BeerRestBase.j
|
||||
----
|
||||
====
|
||||
|
||||
[[features-custom-mode]]
|
||||
=== Custom Mode
|
||||
|
||||
IMPORTANT: This mode is experimental and can change in the future!
|
||||
|
||||
The Spring Cloud Contract allows you to 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 leverages OkHttp3 http client.
|
||||
|
||||
====
|
||||
[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"));
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Corresponding base class could look like this:
|
||||
|
||||
====
|
||||
[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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[features-context-paths]]
|
||||
=== Working with Context Paths
|
||||
|
||||
@@ -564,7 +666,7 @@ contracts {
|
||||
----
|
||||
====
|
||||
|
||||
The base class would set up the applicatoin running on a random port.
|
||||
The base class would set up the application running on a random port.
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes"]
|
||||
@@ -622,5 +724,283 @@ public class BeerControllerGraphQLTest {
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
====
|
||||
|
||||
[[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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
====
|
||||
Reference in New Issue
Block a user