DEV Community

mos_ for Ticino Software Craft

Posted on • Originally published at mosfet.io on

Consumer-Driven Contract Testing with Pact and Java - Part II

The previous post explains the principles and motivations behind contract testing. Today we take a look to how write consumer-driven contract tests with pact and Java in a SpringBoot application.

Pact foundation provides junit5 integration for creation and verification of contracts.

Let’s start!

What you need

The Java Pact Testing framework used in this example is the 4.0.0, based on v3 specification.

Furthermore, you should have:

  • jdk 1.8 or later
  • maven 3.2+
  • a bit of testing knowledge in Spring

You can find all the presented code on github.


The example

example

The proposed example is similar to the previous one seen in part I: we have a provider service which expose anAPI that given a sentence and a timestamp, it replies an echo response enriched with local timestamp. This API _is consumed_by another service. To summarize:

endpoint POST /api/echo

request body

{
    "timestamp": 1593373353,
    "sentence": "hello!"
}

Enter fullscreen mode Exit fullscreen mode

response body

{
    "phrase": "hello! sent at: 1593373353 worked at: 1593373360"
}

Enter fullscreen mode Exit fullscreen mode

Consumer side

We are driven by Consumer, so then we start working on consumer side: we add the pact maven dependency in consumer pom.xml

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-junit5</artifactId>
    <version>4.0.10</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

create a contract

Let’s start creating a junit5 test with PactConsumerTestExt junit extension:


import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(PactConsumerTestExt.class)
class ConsumerContractTest {

Enter fullscreen mode Exit fullscreen mode

in @BeforeEach method we can assert that the mockServer which will serve the contracts is correctly up:

    @BeforeEach
    public void setUp(MockServer mockServer) {
        assertThat(mockServer, is(notNullValue()));
    }

Enter fullscreen mode Exit fullscreen mode

ok, now we can create a contract. A contract can be defined with a method annotated with @Pact that returns a RequestResponsePact and provides as parameter PactDslWithProvider. All methods annotated with @Pact are used toinstrument the mock server through the PactDslWithProvider in this way:

    @Pact(provider = "providerMicroservice", consumer = "consumerMicroservice")
    public RequestResponsePact echoRequest(PactDslWithProvider builder) {

        return builder
                .given("a sentence worked at 1593373360")
                .uponReceiving("an echo request at 1593373353")
                .path(API_ECHO) /* request */
                .method("POST")
                .body(echoRequest) 
                .willRespondWith() /* response */
                .status(200)
                .headers(headers)
                .body(echoResponse)
                .toPact();
    }

Enter fullscreen mode Exit fullscreen mode

The Pact DSL provides a fluent API very similar to Spring mockMvc: Here we are saying that when the mock server receives an echoRequest, it should return 200 and an echoResponse. The given and the uponReceiving method, define the specification in bdd approach and the Pact testing framework, uses the given part to bring the provider into the correct state before executing the interaction defined in the contract.


Matchers: build a response with PactDslJsonBody

in the previous step we created an interaction using two json object(echoRequest and echoResponse). On the provider side, the test verify that the generated response is perfectly equal to the one defined in the contract.

The Pact testing framework provides also a DSL that permits the definition of different matching case in this way:

    @Pact(provider = "providerMicroservice", consumer = "consumerMicroservice")
    public RequestResponsePact echoRequestWithDsl(PactDslWithProvider builder) {

        PactDslJsonBody responseWrittenWithDsl = new PactDslJsonBody()
                .stringType("phrase", "hello! sent at: X worked at: Y") /* match on type */
                .close()
                .asBody();

        return builder
                .given("WITH DSL: a sentence worked at 1593373360")
                .uponReceiving("an echo request at 1593373353")
                .path(API_ECHO)
                .method("POST")
                .body(echoRequest)
                .willRespondWith()
                .status(200)
                .headers(headers)
                .body(responseWrittenWithDsl)
                .toPact();
    }

Enter fullscreen mode Exit fullscreen mode

Here we created a response with PactDslJsonBody DSL that defines a match case based on type instead of value. It’s possible with PactDslJsonBody different match case based on regex or array length.


Verify the contract

Now we can create the real test which verify the contract on the consumer side:

    @Test
    @PactTestFor(pactMethod = "echoRequest")
    @DisplayName("given a sentence with a timestamp, when calling producer microservice, than I receive back an echo sentence with a timestamp")
    void givenASentenceWithATimestampWhenCallingProducerThanReturnAnEchoWithATimestamp(MockServer mockServer) throws IOException {

        BasicHttpEntity bodyRequest = new BasicHttpEntity();
        bodyRequest.setContent(IOUtils.toInputStream(echoRequest, Charset.defaultCharset()));

        expectedResult = new EchoResponse();
        expectedResult.setPhrase("hello! sent at: 1593373353 worked at: 1593373360");

        HttpResponse httpResponse = Request.Post(mockServer.getUrl() + API_ECHO)
                .body(bodyRequest)
                .execute()
                .returnResponse();

        ObjectMapper objectMapper = new ObjectMapper();
        EchoResponse actualResult = objectMapper.readValue(httpResponse.getEntity().getContent(), EchoResponse.class);

        assertEquals(expectedResult, actualResult);
    }

Enter fullscreen mode Exit fullscreen mode

if we run mvn test and we don’t have errors, we will see in ./target/pacts a json file that use the pact formalism for contracts. We use the generated contract in the provider-side.


Provider side

For the provider, we have a different dependency to add in pom.xml:

        <dependency>
            <groupId>au.com.dius</groupId>
            <artifactId>pact-jvm-provider-junit5</artifactId>
            <version>4.0.10</version>
        </dependency>

Enter fullscreen mode Exit fullscreen mode

Verify the contract on provider side

Here is the thing: we need to verify the contract against provider implementation. In the Spring world, it’s sounds like an integration test which verify the web layer. So here the magic:

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = ProviderApplication.class)
@EnableAutoConfiguration
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-contract-test.properties")
@Provider("providerMicroservice")
@PactFolder("../consumer/target/pacts")
public class ProviderContractTest {

    @Value("${server.host}")
    private String serverHost;
    @Value("${server.port}")
    private int serverPort;

    @BeforeEach
    void setupTestTarget(PactVerificationContext context) {
        context.setTarget(new HttpTestTarget(serverHost, serverPort, "/"));
    }

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void pactVerificationTestTemplate(PactVerificationContext context) {
        context.verifyInteraction();
    }

    @State("a sentence worked at 1593373360")
    public void sentenceWorkedAt1593373360() {
        when(phraseService.echo(1593373353, "hello!"))
                .thenReturn(new Phrase("hello! sent at: 1593373353 worked at: 1593373360"));
    }

}

Enter fullscreen mode Exit fullscreen mode

That’s it. As you can see, we have a @SpringBootTest with a fixed port and a @TestPropertySource that defines it in order to attach the pact context to the application context with host and port info.Obviously there are other ways, like random ports and so on, but the main thing here is to bind both context together.

Another thing here is the @PactFolder annotation that points to contracts generated by the consumer. The Pact Framework search for contracts that belong to the service, and run the verification.


The @State annotation

    @State("a sentence worked at 1593373360")
    public void sentenceWorkedAt1593373360() {
        when(phraseService.echo(1593373353, "hello!"))
                .thenReturn(new Phrase("hello! sent at: 1593373353 worked at: 1593373360"));
    }

Enter fullscreen mode Exit fullscreen mode

As previously mentioned, the given statement in the consumer contract, define with a business expression, the state in which the system-under-test, should be during the execution. Following this approach, we define in the provider, a method with @state annotation, that contains the commands necessary for the correct execution. In our case, we mock the business service delegated to execute the eco logic. The framework executes the state method before calling the API defined in the contracts. The real test, in this way, is “reduced” to a simple call:


    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void pactVerificationTestTemplate(PactVerificationContext context) {
        context.verifyInteraction();
    }

Enter fullscreen mode Exit fullscreen mode

Use a broker

If you have a broker that stores the contracts, you can change the @PactFolder annotation with @PactBroker one and define the following plugin in the pom.xml:

    <build>
        <plugins>
            <plugin>
                <groupId>au.com.dius</groupId>
                <artifactId>pact-jvm-provider-maven</artifactId>
                <version>4.0.0</version>
                <configuration>
                    <pactDirectory>target/pacts</pactDirectory>
                    <pactBrokerUrl>${pact.broker.protocol}://${pact.broker.host}</pactBrokerUrl>
                    <projectVersion>${contracts.version}</projectVersion>
                    <trimSnapshot>true</trimSnapshot>
                </configuration>
            </plugin>
        </plugins>
    </build>

Enter fullscreen mode Exit fullscreen mode

What’s Next

We have covered how to develop and verify a simple contract with java starting from the consumer. We have used the Pact DSL and matchers introduced in v3 spec which is an interesting feature during design & testing. As previously mentioned, you can find a complete working example in this github repo.

In the next posts(I hope), we will see how deploy a broker server to store contracts and how integrate the entire flow witha CI.

The original post can be found here

Top comments (0)