Spring WebFlux Support in Pact JVM
Join the DZone community and get the full member experience.
Join For FreePact JVM has had Spring support for a while but on the provider side it was limited only to mocked Spring MVC. Starting with version 4.1.7 Pact also supports Spring WebFlux endpoints.
In this article I'll demonstrate usage of Pact Consumer Driven Contracts to test Spring WebFlux provider endpoints and its consumers. I'll start with a short introduction to consumer driven contract testing and Spring WebFlux. Then I'll explain how to connect those two technologies together to produce a contract and with that contract verify both consumer and provider of a service.
What Is Pact Consumer Driven Contract Testing
Consumer Driven Contract Testing is a pattern for testing compatibility between consumers and providers of services. The idea is that between those two there's established a pact describing their interactions. If both parties fulfills assumptions made in the pact, a test passes successfully.
"Consumer driven" approach means that the force responsible for contract evolution are consumers of the service, not a provider.
DiUS Pact is a set of testing frameworks supporting Consumer Driven Contract Testing. It focuses mostly on HTTP communication but for some platforms also queueing support is available. Implementations are targeting a majority of popular runtime environments like JVM, .NET, JavaScript, Ruby to name a few.
What Is Spring WebFlux
Spring WebFlux is a reactive web framework introduced in Spring 5. It is a counterpart of the well known Spring MVC but relies on non-blocking APIs provided by reactive streams. The framework is capable of handling heavy loads on a small number of threads while consuming fewer hardware resources.
WebFlux offers two programming models:
- annotated controllers
- functional endpoints
Although Pact supports both in this article I'll focus on functional endpoints style.
How Does It Work Together
As it is a consumer responsibility to provide the contract, pact tests are written for the consumer at first. Each of those tests contains description of interactions between consumer and provider, e.g. HTTP requests, expected responses and assertions verifying communication. Pact framework takes care of starting a mock HTTP server equipped with mock responses and executes tests against that server.
When a test is executed, Pact framework creates a dedicated contract (JSON) file. The same file is then used to check if the provider of the service conforms to the pact. On the server side, the framework will execute requests recorded in the contract file and verify responses. We can choose to execute a test against the service as a whole or only its slice responsible for HTTP communication. In this article I will cover the letter.
Client Side Test
Let's assume that the provider exposes an HTTP endpoint. It responds to HTTP GET method with JSON representation of Foo objects collection. Let’s use the Spock framework to code the consumer pact test.
First we have to define a pact inside the given section of our test. For that we will use ConsumerPactBuilder class, provided by Pact library:
xxxxxxxxxx
given:
def pact = ConsumerPactBuilder.consumer("consumerService")
.hasPactWith("providerService")
.uponReceiving("sample request")
.method("GET")
.path("/foo")
.willRespondWith()
.status(200)
.headers(["Content-Type": "application/json"])
.body("""
[
{"id": 1, "name": "Foo"},
{"id": 2, "name": "Bar"}
]
""".stripIndent())
.toPact()
We can see here that contract defines interaction named sample request between consumer consumerService and provider providerService. As a reaction to HTTP GET request on /foo path, provider should respond with HTTP status 200 and response’s body section should contain representation of two Foo objects.
Now let's code assertions part and actual invocation of our client in when section:
xxxxxxxxxx
when:
def result = ConsumerPactRunnerKt.runConsumerTest(
pact, MockProviderConfig.createDefault()) { mockServer, context ->
def webClient = WebClient.create(mockServer.getUrl())
def consumerAdapter = new ConsumerAdapter(webClient)
def resultFlux = consumerAdapter.invokeProvider()
StepVerifier.create(resultFlux)
.expectNext(new Foo(1l, 'Foo'))
.expectNext(new Foo(2l, 'Bar'))
.verifyComplete()
}
To execute the test we will use the ConsumerPactRunnerKt class provided by Pact library. Before executing code in the closure, method runConsumerTest will start a temporary server that will respond to HTTP requests defined earlier in the pact. It will also save a contract in the form of a JSON file that would be shared with the service provider. The last parameter of the mentioned method is a code block containing a call to our service client. In this case we will initiate a reactive webClient preconfigured with a mock HTTP server URL. Then we will create an instance of our adapter (ConsumerAdapter) and call invokeProvider() method responsible for HTTP interaction with the provider. As the result of that interaction would be reactive Flux we can use StepVerifier from the Reactor project to verify its correctness.
The final part would be to check if pact verification has passed in then section:
xxxxxxxxxx
then:
result == new PactVerificationResult.Ok()
Here we can check if StepVerifier didn’t generate an exception (it would be wrapped inside PactVerificationResult.Error class), some mandatory interaction wasn’t performed or was inconsistent with contract definitions.
Let’s write the code for the sample consumer used in our test. It would be conventional usage of reactive WebClient for reading data from /foo endpoint:
xxxxxxxxxx
public Flux<Foo> invokeProvider() {
return webClient
.get()
.uri("/foo")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(Foo.class);
}
Now we are ready to run the test. It will start a Pact provided HTTP server, invoke that server from the client's adapter and verify the response provided by client class. As an additional outcome of the test Pact framework will produce JSON representation of pact in build/pacts directory. We should then share that contract file with tests verifying a provider of the service.
Server Side Test
As we already have the pact file generated, the integration test for a provider would be a bit more straightforward. This time we will use JUnit framework as Spock's given–when–then pattern wouldn't be very useful here:
xxxxxxxxxx
RestPactRunner.class) (
"providerService") (
"pacts") (
public class ProviderRouterPactTest {
public WebFluxTarget target = new WebFluxTarget();
private ProviderHandler handler = new ProviderHandler();
private RouterFunction<ServerResponse> routerFunction
= new ProviderRouter(handler).routes();
public void setup() {
target.setRouterFunction(routerFunction);
}
}
As we can notice code doesn't contain any test methods. It’s because test requests and assertions are already present in the contract file. SpringRestPactRunner (pointed in @RunWith annotation) will take care of executing pact test cases and verification of responses. We have to yet inform the test runner about location of contract files (in this case with @PactFolder annotation we are pointing directory on disk) and name of our provider (with @Provider annotation). Pact engine will use that name to match contracts dedicated to test particular providers. It is also used in consumer tests inside pact definition, e.g. hasPactWith("providerService").
The @TestTarget annotation is responsible for choosing the type of tested component. In the case of WebFlux endpoints it would be an instance of WebFluxTarget. It should be configured with the routerFunction that we use in our provider's code. Convenient place to do it would be the setup() method of a test.
We should also mention ProviderHandler and ProviderRouter classes. These are implementation parts of the sample provider. Router is responsible for building RouterFunction instance that binds URL paths with code of the handler:
xxxxxxxxxx
level = AccessLevel.PRIVATE, makeFinal = true) (
class ProviderRouter {
ProviderHandler handler;
RouterFunction<ServerResponse> routes() {
return route()
.GET("/foo", accept(APPLICATION_JSON), handler::getFoo)
.build();
}
}
The handler::getFoo method from sample above is responsible for returning Mono with provider’s response:
Mono<ServerResponse> getFoo(ServerRequest request) {
return ServerResponse
.ok()
.contentType(APPLICATION_JSON)
.body(Flux.just(
new Foo(1l, "Foo"),
new Foo(2l, "Bar")
), Foo.class);
}
The handler method returns two Foo objects as it is expected by the contract between provider and consumer.
Running the provider pact test will now load the contract file, invoke a handler matched by URL path and verify if the generated response is consistent with the one stated in the contract.
Summary
Pact Consumer Driven Contracts is a very useful tool for ensuring consistency between elements of a complex system. Certainly, its advantage is the wide range of supported technologies, which allows for contract-based testing in a heterogeneous environment. Now yet another technology has joined the framework - the Spring WebFlux, thanks to that we have gained the ability to perform tests against reactive providers.
Source code for examples used in this article is available in Github repository.
References
- Pact JVM Github page
- Spring WebFlux documentation
- Consumer-Driven Contracts: A Service Evolution Pattern, Ian Robinson
- Pact WebFlux integration example on Github
Published at DZone with permission of Paweł Pelczar. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments