Effectively running Java on ServerlessUnit Testing4 min

Unit tests validate individual units of your code in isolation, hence independently of any other components such as DynamoDb or API Gateway.

To facilitate unit test of the handler function, move as much of logic as possible to other functions that are then called by the Lambda handler entry point. In our example, we delegate the persistence of the ticket to the method ddbUtils.persistTicket(ticket) of the class DDBUtils. The purpose of the DDBUtils class is to wrap all the API calls to DynamoDB.

Mocking AWS API Calls

In the production code, we need to create a real DynamoDB client to interact with DynamoDB. The test code instead being performed in isolation, does not require any interaction with a real database. By mocking an instance of DDBUtils, we can simulate the positive and negative scenarios using the client.

To allow both production and test code execution, we need to define a constructor which takes an instance of ddbUtils as argument.

public class TicketFunction {
  private DDBUtils ddbUtils;

  public TicketFunction(DDBUtils ddbUtils) {
    if (ddbUtils == null) {
      DynamoDbClient ddb = DDBUtils.getDynamoDbClient();
      DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
        .dynamoDbClient(ddb)
        .build();
      this.ddbUtils = new DDBUtils(enhancedClient);
    } else {
      this.ddbUtils = ddbUtils;
    }
  }
}

The test code will create a mocked instance of DDBUtils and pass it to the TicketFunction constructor; instead the production code will pass null to the TicketFunction constructor so that it created the DynamoDbClient and DynamoDbEnhancedClient. When Lambda instantiates the TestFunction, it calls the default constructor without any argument, therefore, we need to override the default constructor to invoke the constructor which we just defined passing null as input:

public TicketFunction() {
  this(null);
}

To write our unit tests, we will use JUnit, which provides handy assertions and annotations for our tests, and Mockito, a popular Java framework that we can use to mock the AWS Services API calls wrapped in the DDBUtils class.

public class TicketFunctionMockTest {

  @Mock
  private transient DDBUtils testUtils;
  
  private TicketFunction function;
  
  @BeforeEach
  public void beforeEach() {
    MockitoAnnotations.openMocks(this);
    function = new TicketFunction(testUtils);
  }
}

Before each test we instantiate a new TicketFunction with the mocked DDBUtils class, so that we can inject a custom behavior in the persistTicket method. For example, the following statement that it will tell the mock to return a specific uuid when the DDBUtils.persistTicket() method is called.

Mockito.when(
  testUtils.persistTicket(
    Mockito.any(Ticket.class)
  )
)
.thenReturn(uuid);

Mocking integrations with other AWS Services

So far we have seen how to mock classes we defined to wrap API calls to AWS Services such as DynamoDB, but the behavior of our Lambda function varies depending on the input events received from other AWS Services, API Gateway in our example.

Our unit tests then can feed custom input events to our Lambda Function and test the behavior in isolation as we are mocking any API call to AWS Services. It is the best practice to define these input events as json files stored in the src/test/resources/events folder.

We will use the aws-lambda-java-tests library to seamlessly load and deserialize JSON events and inject them in your tests.

The @Event annotation provides loaders for the most common event types available in aws-lambda-java-events. It requires the path of a json file with the event definition from the src/test/resources folder, and the aws-lambda-java-events type to map the event with.

We use the JUnit5 ParameterizedTest annotation to inject arguments into a test, so we can run the same test one or more times with different parameters:

@ParameterizedTest
@Event(value = "events/apigw_request_1.json", type = APIGatewayProxyRequestEvent.class)
public void testEventDeserialization(APIGatewayProxyRequestEvent event) {
  uuid = UUID.randomUUID().toString();
  Mockito.when(testUtils.persistTicket(Mockito.any(Ticket.class))).thenReturn(uuid);
  
  APIGatewayProxyResponseEvent response = function.handleRequest(event, null);
  
  ArgumentCaptor<Ticket> ticketArgumentCaptor = ArgumentCaptor.forClass(Ticket.class);
  Mockito.verify(testUtils).persistTicket(ticketArgumentCaptor.capture());
  
  Assertions.assertEquals("Lambda rocks", ticketArgumentCaptor.getValue().getDescription());
  Assertions.assertEquals("testuser", ticketArgumentCaptor.getValue().getUserId());
}

You can find the complete Unit Test code for this example in the GitHub repository serverless-test-samples.


Created by:

Lorenzo Micheli
Lorenzo MicheliLorenzo is a Principal Cloud Architect in AWS Professional Services. He is passionate about Software Engineering, Serverless and Event-Driven Architectures.