Test Spring Applications Using AWS With Testcontainers and LocalStack

Last Updated:  August 27, 2021 | Published: April 21, 2020

If your Spring application uses AWS components like S3, SNS, or SQS you might wonder how to write effective integration tests. Should I mock the entire interaction with AWS? Should I duplicate the AWS infrastructure for testing? Is there maybe a local AWS clone available? With this blog post, you'll learn how to write integration tests for your Spring Boot application integrating AWS service with the help of Testcontainers and LocalStack. The demo application uses both SQS and S3 to process order events for which we'll write an integration test.

UPDATE: With more recent versions of LocalStack, Testcontainers, and Spring Cloud AWS, the required integration test setup is more streamlined. More about this in an upcoming blog post.

Spring Boot Application Setup

The application is a basic Spring Boot project using Java 11 and Spring Cloud. We're using the spring-boot-starter-aws-messaging to have almost no setup for connecting to an SQS queue. Furthermore, this messaging starter has a transitive dependency to spring-cloud-starter-aws that provides the core AWS support:

For writing integration tests we need the following test dependencies:

Let's have a look at why we need these dependencies and what they are doing.

As our demo application makes use of AWS services (S3 and SQS), we have to provide this infrastructure for the integration tests. We can either duplicate the AWS services for our tests or provide a mock infrastructure. Option one is using real AWS services for our tests and hence making sure the application can work with them. It has the downside of additional AWS costs and once two developers execute the integration tests, they might run into issues.

With option two we have the drawback of using a mock infrastructure and a more artificial environment (e.g. almost no latency, not using the real AWS services, …).  On the other hand, it's cheap and can run on multiple machines in parallel. For this tutorial we're choosing option two and make use of LocalStack:

  • Easy-to-use test/mocking framework for developing AWS applications
  • It spins up an environment with the same functionality and APIs as AWS
  • Available as Docker containers and supports all core AWS services in the community edition

To manage the lifecycle of the LocalStack Docker container, we're using Testcontainers:

Next, as we want to verify the main functionality of the application (processing of SQS messages), we have to test asynchronous code. Fortunately, there is a great Java library available for this Awaitility:

  • Java Utility library to test asynchronous code
  • Express expectations using a DSL

Finally, the Spring Boot Starter Test serves multiple purposes:

  • Swiss-army knife for testing Spring applications
  • Includes basic testing infrastructure: test engine, assertion libraries, mocking library, etc.
  • Exclude the junit-vintage-engine to only use JUnit 5

Spring Boot Application Walkthrough

First, let's have a look at how the connection to AWS is configured. The Java AWS SDK offers multiple ways (e.g. system properties, env variables, etc.) to configure the AWS credentials. As we are using the Spring Cloud Starter for AWS, we can additionally configure them the Spring Boot-way by specifying both the access and secret key inside our application.yml :

Apart from this, we're also specifying the AWS region and both SQS and S3 logical resource names. If we're using the Parameter Store of the AWS Systems Manager (SSM), we can also define such configuration values in AWS. As the application is not running inside an AWS stack (e.g. EC2), automatic stack detection is disabled with cloud.aws.stack.auto.

The actual processing logic of this demo application happens inside the SQS listener. As we are using the messaging starter for AWS, we can easily subscribe to an SQS queue using @SqsListener. The logic for each incoming message is dead-simple. We'll log each incoming OrderEvent and upload it to S3.

The OrderEvent is a POJO containing information about the order:

While the raw SQS payload is a String, the AWS messaging dependency allows us to serialize it to a Java object. We are already doing this, as the processMessage method takes OrderEvent as a parameter. Behind the scenes, this conversion is done using the MappingJackson2MessageConverter.

By default, this message converter instantiates its own Jackson ObjectMapper. As the OrderEvent uses a Java 8 LocalDateTime we need the Java Time Module registered inside the ObjectMapper.

To override this default behavior, we can provide our own MappingJackson2MessageConverter and set the ObjectMapper. We're using the auto-configured ObjectMapper from Spring Boot for this as it contains all required Jackson modules out-of-the-box:

The second bean inside this configuration is optional, but we'll use the QueueMessagingTemplate to send a message to SQS during test execution.

Next, let's see how we can write integration tests for our Spring application using these AWS components with Testcontainers and LocalStack.

Integration Test Setup with Testcontainers and LocalStack

Now we can focus on testing this application logic with an integration test. During the test execution, we need access to SQS and S3. For this we'll use the LocalStack module of Testcontainers and use the JUnit Jupiter extension to manage the lifecycle of the LocalStack Docker container:

As our application expects an SQS queue to subscribe to and an S3 bucket to write data to, we need to create both resources. We can perform additional setup tasks inside the Docker container using the execInContainer method from Testcontainers. We can use this mechanism to create the infrastructure using awslocal (thin AWS CLI wrapper of LocalStack).

As part of the JUnit Jupiter lifecycle@BeforeAll, we prepare the LocalStack environment before any test is executed.

As the queue and bucket names differ from the ones we used for the production profile, we can specify the new names inside src/test/resources/application.yml:

Verify the AWS SQS Message Processing of The Spring Application

The remaining setup for our test is to configure the AWS clients properly. Without manually specifying the required clients as Spring beans, the auto-configure mechanism would try to connect to the real AWS cloud during application startup.

As we don't want to connect to the actual AWS services and rather use our mocked environment of LocalStack, we can provide the beans for ourselves a @TestConfiguration class :

Both clients will now point to the local AWS S3 and AWS SQS instances. As our @TestConfiguration class is an inner static class of our integration test, it will be detected automatically. There's no need to explicitly use @Import(AwsTestConfig.class) unless we outsource it to a dedicated file.

Finally, we can now write the test for our order event processing.

First, we put a message into the local AWS SQS queue using the QueueMessagingTemplate and then expect to find an object in the S3 bucket with the given order id:

The given() part comes from Awaitility. As this message processing happens asynchronously, we can't expect the object to be present in S3 right after we put the message in the queue. Therefore we'll wait for five seconds to find the object in S3 and otherwise fail the test.

The .ignoreException() part is necessary, as the S3 client will throw this exception whenever it can't find the request object, which might be the case in the first milliseconds of trying to find it.

Summary

With the setup above you are now able to write integration tests for your Spring application and can include AWS components using LocalStack and Testcontainers. Furthermore, this setup also works for any other AWS component that LocalStack supports e.g. DynamoDB or SNS.

The demo application is available on GitHub.

If you’re interested in learning more about building applications with Spring Boot and AWS from top to bottom, make sure to take a look at the Stratospheric project. With this book, you'll learn all you need to know to get your Spring Boot application into production with AWS and how to effectively integrate multiple AWS services. (PS: I'm co-authoring this book)

Have fun writing integration tests for your Spring application using AWS with LocalStack and Testcontainers,

Phil

  • Hi! Thanks for this! It’s been helpful to try and get something setup. My only issue is that when the bean AmazonSQSAsync is created in the TestConfiguration it doesn’t override the bean created in the actual application (which has to be marked as primary to override the default Spring one). This means it always tries to connect to the actual bean. Is there a way to make sure the test bean overrides the actual bean for testing?

    • Hey Dexter,

      yes, just recently I found a better way of doing it and also solving your request. You can put the following inside your application.properties file inside src/main/test

      spring.main.allow-bean-definition-overriding=true

      This will override the bean and you won’t have to use @Primary at all

  • Great article, thanks.

    Just one question, in my @SpringBootTest then @SqsListener is unable to connect to the queue. Any idea how to resolve this?

    SimpleMessageListenerContainer : An Exception occurred while polling queue 'my-queue'. The failing operation will be retried in 10000 milliseconds
    com.amazonaws.SdkClientException: Unable to execute HTTP request: Connect to localhost:56734 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused (Connection refused)

    • Hi David,

      thanks for your feedback.

      I’m happy to help – could you please either create a GitHub issue or a Stack Overflow question and send me the link as a reply to this comment? Please also add your existing test setup and further information about your project (e.g. dependency version, etc.).

      The functionality of my blog’s comment section is quite limited to properly paste large code examples and seamlessly interact.

      Kind regards,
      Philip

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

    Join Our Mailing List To Get 3x Free Cheat Sheets

    Free Java Cheat Sheets
    >