Sunday, April 5, 2020

PACT provider test with Serverless Java 11 Lambda and JUnit 5 (Jupiter)

Introduction

This blogpost shows how to create a Pact provider test using:
  1. Pact 4.0.0
  2. AWS Serverless Lambda
  3. Java 11
  4. JUnit 5 (Jupiter)

Example Provider Test

Below is the example test class. Note how a mock service is used to simulate the endpoint defined (also) in serverless.yml.

package com.ttlnews.pact.tests.provider;

import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactBroker;
import au.com.dius.pact.provider.junit.loader.PactBrokerAuth;
import au.com.dius.pact.provider.junit5.HttpTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import com.amazonaws.services.lambda.runtime.Context;
import lombok.extern.slf4j.Slf4j;
import net.jcip.annotations.NotThreadSafe;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockserver.integration.ClientAndServer;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockserver.integration.ClientAndServer.startClientAndServer;
import static org.mockserver.matchers.Times.exactly;
import static org.mockserver.model.Header.header;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;

/**
 * Java 11 serverless provider Pact contract provider implementation using Pact with Junit5 (Jupiter) for provider SERVICE_A.
 * 
 * Dependencies needed:
 *
     <dependency>
     <groupId>au.com.dius</groupId>
     <artifactId>pact-jvm-consumer-junit5</artifactId>
     <version>4.0.0</version>
     <scope>test</scope>
     </dependency>
    
     <dependency>
     <groupId>au.com.dius</groupId>
     <artifactId>pact-jvm-provider-junit5</artifactId>
     <version>4.0.0</version>
     <scope>test</scope>
     </dependency>
 *
 * 
 */
@Provider(SERVICE_A)
// Below environment variables must be set when running this test
@PactBroker(host = "${PACT_HOST}",
        authentication = @PactBrokerAuth(username = "${PACT_HOST_USERNAME}", password = "${PACT_HOST_PASSWD}"))
@NotThreadSafe // Pact contract tests can't seem to handle methods running in parallel, so prevent maven failsafe/surefire plugin to run Pact tests in parallel
@Slf4j
public class ServiceAProviderTest {

    private static final String SERVICE_A = "service-A";
    
    private Context lambdaContext;
    private SomeRepository someRepository;

    private ClientAndServer mockClientAndServer;

    @BeforeEach
    void before(PactVerificationContext context) {

        lambdaContext = mock(Context.class);
        someRepository = new InMemoryMockedRepo();

        mockClientAndServer = startClientAndServer(8888);

        context.setTarget(new HttpTestTarget("127.0.0.1", 8888));

    }

    @AfterEach
    void stopServer() {
        mockClientAndServer.stop();
    }

    // This triggers the defined contract test(s) at the host PACT_HOST
    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void pactVerificationTestTemplate(PactVerificationContext context) {
        context.verifyInteraction();
    }

    @State("Create a new resource A")
    public void shouldCreateResourceA() {

        log.info("Set up state");

        // Given
        // Maybe some more mocking of 'lambdaContext' needed, depending on your case
        
        // Lambda being tested
        CreateResourceALambda createResourceALambda = new CreateResourceA(someRepository);

        CreateResourceALambdaRequest createResourceALambdaRequest = CreateResourceALambdaRequest.builder()
                .someValue("25")
                .build();

        // When
        final LambdaResult lambdaResult = createResourceALambda.executeRequest(createResourceALambdaRequest, lambdaContext);
        final String body = lambdaResult.getBody();
        assertNotNull(body);

        // Prepare the mockserver to return what the lambda returns when it is invoked (set up above in the 'body' variable) 
        // Of course the path to this CreateResourceALambda is defined in the serverless.yml, but that we can't access now. So need to repeat that
        // endpoint here.
        mockClientAndServer.when(
                request()
                        .withMethod("POST")
                        .withPath("/resources/")
                        .withHeaders(

                                header("x-request-trace-id"),  // Any value is fine
                                header("Authorization"),  // Any value is fine
                                header("Content-Type", "application/json")

                        )
                        .withBody(json("{someValue: '25'}")) // Indeed need to use this strange JSON format
                ,
                exactly(1))
                .respond(
                        // Response as defined by the matching Pact consumer test; in this case found at host PACT_HOST
                        response()
                                .withStatusCode(201)
                                .withHeaders(
                                        header("Content-type", "application/json; charset=utf-8"),
                                        header("Authorization") // Any value is fine
                                )
                                .withBody(body)
                );
    }
}
 
Migrating from your Pact tests from JUnit4 to Junit5 can be found here.
 
Note to self: use https://www.opinionatedgeek.com/codecs/htmlencoder to encode code, open the HTML view, and then wrap the code in <pre> open en close tag.