Effectively running Java on ServerlessIntegration Testing5 min

Once the AWS Lambda code has been tested in isolation, we need to make sure that it works together with the other services it interacts with. Some functionalities can be delegated entirely to managed services such as Amazon S3, or created without using any internally developed code. There is no need to test these managed services, but you do need to test the integration with these services.

Testing with Emulators

One way to test the integration with cloud managed services is using Emulator, a locally running application which mimics a production AWS service.

Emulators have APIs that are similar to their cloud counterparts and provide similar return values. They can also simulate state changes that are initiated by API calls. For example, you might use AWS SAM to run a function with AWS SAM local to emulate the Lambda service so that you can quickly invoke a function. See AWS SAM local in the AWS Serverless Application Model Developer Guide for details.

The advantages of test with emulators include the following:

  • Emulators can facilitate fast local development iterations and testing.
  • Emulators provide a familiar environment for developers used to developing code in a local environment.
  • Emulators do not require any changes to cloud infrastructure (such as developer cloud accounts), so it’s easy to implement with existing testing patterns.

Testing with emulators has these disadvantages:

  • Emulators can be difficult to set up and replicate, especially when used in CI/CD pipelines.
  • Emulated features and APIs typically lag behind service updates. This can lead to errors because tested code does not match the actual API, and impedes the adoption of new features.
  • Emulators require support, updates, bug fixes, and feature parity enhancements. These are the responsibility of the emulator author, which could be a third-party company.
  • Tests that rely on emulators may provide successful results locally, but fail in the cloud due to production security policies, inter-service configurations, or exceeding Lambda quotas.
  • Many AWS services do not have emulators available. If you rely on emulation, you may not have a satisfactory testing option for portions of your application.

Integration Testing with actual resources in the cloud

You can run integration tests against cloud-based code that also interacts with cloud-based services.

Although the test runs locally, it interacts with cloud-based resources. These resources have been deployed using the AWS Serverless Application Model and AWS SAM command line tool.

To demonstrate how we can implement and run Integration Tests in the cloud for the TicketFunction, we will use the code in TicketFunctionIntegrationTest.java.

This tests assumes that the DynamoDB table is provisioned in your AWS account, and you have the appropriate credentials locally to access this account (for example in ~/.aws/credentials). To do that, you need first to deploy the SAM Template provided in the repository using the commands sam build and sam deploy as follows:

$ sam build
...
Build Succeeded

$ sam deploy --guided
Configuring SAM deploy
======================

  Looking for config file [samconfig.toml] :  Found
  Reading default arguments  :  Success

  Setting default arguments for 'sam deploy'
  =========================================
  Stack Name [apigw-lambda-ddb]: apigw-lambda-ddb
  AWS Region [us-east-2]: us-east-2
  ....

Make sure to specify the AWS Region us-east-2 as TicketFunctionIntegrationTest.java assumes that the DynamoDb table is deployed in us-east-2

Mock event payload but integrate with AWS Services in the cloud

So far we have seen how to write unit tests by mocking classes we defined to wrap API calls to AWS Services such as DynamoDB. In an integration test, you still will mock the event payload but instead of mocking the DynamoDB APIs, you will actually call the real APIs on cloud.

Our integration tests then can feed custom input events to our Lambda Function and test the behavior in real as we are calling the actual API 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.

Similar to unit tests, 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.

The major difference in integration tests compared to unit tests is that the actual DynamoDB database on AWS will be used in the test. Therefore, make sure the AWS credentials used in the tests are valid and a DynamoDBClient is available in the same region as the database for assertion.

@ExtendWith(SystemStubsExtension.class)
public class TicketFunctionIntegrationTest {

  private static final Region TEST_REGION = Region.US_EAST_2;
  @SystemStub
  private static EnvironmentVariables environmentVariables;

  private final DynamoDbClient ddbClient = DynamoDbClient.builder()
    .region(TEST_REGION)
    .build();
  
  private List<String> ticketList = new ArrayList<>();

  @BeforeAll
  public static void setup() {
    ProfileCredentialsProvider credentialsProvider = ProfileCredentialsProvider.create("default");
    AwsCredentials awsCredentials = credentialsProvider.resolveCredentials();
    environmentVariables.set("AWS_ACCESS_KEY_ID", awsCredentials.accessKeyId());
    environmentVariables.set("AWS_SECRET_ACCESS_KEY", awsCredentials.secretAccessKey());
    environmentVariables.set("AWS_REGION", TEST_REGION.id());
  }

  @ParameterizedTest
  @Event(value = "events/apigw_request_1.json", type = APIGatewayProxyRequestEvent.class)
  public void testPutTicket(APIGatewayProxyRequestEvent event, EnvironmentVariables environmentVariables) {
    TicketFunction function = new TicketFunction();

    // Note there is no mocking API used before calling the handler
    APIGatewayProxyResponseEvent response = function.handleRequest(event, null);
    Assertions.assertNotNull(response);
    Assertions.assertNotNull(response.getBody());

    String uuidStr = response.getBody();
    Assertions.assertNotNull(uuidStr);

    ticketList.add(uuidStr.substring(1, uuidStr.length() - 1));
    DynamoTestUtil.validateItems(ticketList, ddbClient);
  }
}

You can find the complete integration test code for this example in TicketFunctionIntegrationTest.java and all the other examples in the same GitHub repository serverless-test-samples.


Created by:

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