Search results
Provisioning SNS topics, SQS queues to run integration tests with Testcontainers, LocalStack and Docker for Spring Boot applications
1. OVERVIEW
Message brokers and messaging systems are fundamental to building resilient distributed applications.
Both AWS SQS and SNS are messaging systems that facilitate asynchronous communication between different parts of a distributed architecture.
Your organization’s Spring Boot applications use different messaging approaches: certain applications send SNS notifications, others publish messages to SQS queues, while additional applications consume messages from SQS queues or process SNS notifications that have been bridged into SQS.
Along with writing unit tests for REST controllers, business logic, etc., you should consider writing integration tests to verify the interactions between different systems work well.
This blog post helps you write a custom Spring’s TestExecutionListener to automate provisioning SQS queues and SNS topics for Spring Boot integration tests, ensuring each test starts from a known state and can run independently in any order.
Additionally, this blog post covers writing a custom Jupiter/JUnit 5 Extension to share a Testcontainers LocalStack instance across your SNS, SQS-related integration test suite, instead of each integration test class starting a new LocalStack Docker container.
Provision SNS topics and SQS queues for testing Spring Boot applications with Testcontainers, LocalStack and JUnit 5 Extension
The test code snippets we’ll discuss here are based on an upcoming blog post: Consuming AWS SQS Messages with Spring Boot, AWS Java SDK
v2
, and spring-cloud-aws-starter-sqs
, where the SQS messages come from an SQS publisher application, or from an SQS queue subscribed to an SNS topic.
Getting started with Spring Boot, DynamoDB, AWS Java SDK v2.
Provisioning AWS DynamoDB tables and seeding data to run Spring Boot Integration Tests.
Publishing AWS SNS Notifications with Spring Boot, AWS Java SDK v2.
2. MAVEN DEPENDENCIES
pom.xml
:
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath />
</parent>
<properties>
<java.version>21</java.version>
<spring-cloud-aws.version>3.4.0</spring-cloud-aws.version>
</properties>
<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-sqs</artifactId>
</dependency>
...
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-sns</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>localstack</artifactId>
<scope>test</scope>
</dependency>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-dependencies</artifactId>
<version>${spring-cloud-aws.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
-
This Spring Boot application uses version
3.5.4
. -
AWS BOM’s
spring-cloud-aws-dependencies
is set to3.4.0
.
This is the most recent version you can use with Spring Boot3.5.x
according to the compatibility table below. -
spring-cloud-aws-starter-sqs
is one of the libraries managed byspring-cloud-aws-dependencies
, and it helps you write AWS SQS publishers and listeners for Spring Boot applications. -
spring-cloud-aws-starter-sns
is added as atest
dependency because the code snippets discussed in this post are based on a Spring Boot SQS subscriber application.
The integration tests cover both scenarios: consuming messages published directly to SQS queues and consuming SQS messages routed through SNS subscriptions.
If your Spring Boot application already includesspring-cloud-aws-starter-sns
, you don’t have to add it as atest
dependency. -
org.testcontainers:junit-jupiter
brings support for @Testcontainers class-level annotation and the TestcontainersExtension Jupiter/JUnit5
Extension. -
org.testcontainers:localstack
brings in the LocalStack module allowing you to develop and/or test your Java applications using AWS services like S3, SNS, SQS, and more.
Spring Boot, Spring Cloud AWS, and AWS Java SDK compatibility table
Spring Cloud AWS | Spring Boot | Spring Cloud | AWS Java SDK |
---|---|---|---|
3.0.x | 3.0.x, 3.1.x | 2022.0.x (4.0/Kilburn) | 2.x |
3.1.x | 3.2.x | 2023.0.x (4.0/Kilburn) | 2.x |
3.2.x | 3.2.x, 3.3.x | 2023.0.x (4.0/Kilburn) | 2.x |
3.3.x | 3.4.x | 2024.0.x | 2.x |
3.4.x | 3.5.x | 2025.0.x | 2.x |
Souce: https://github.com/awspring/spring-cloud-aws
3. TESTEXECUTIONLISTENER SUPPORTING CODE
This custom Spring’s TestExecutionListener implementation hooks into the integration test life cycle to provision SQS queues and SNS topics with subscriptions before each test runs, and to delete them after each test completes.
MessagingTestExecutionListener.java
:
public class MessagingTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
super.beforeTestMethod(testContext);
this.executeMessagingOperation(testContext, Messaging.ExecutionPhase.BEFORE_TEST_METHOD);
}
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
RuntimeException resultingException = null;
try {
this.executeMessagingOperation(testContext, Messaging.ExecutionPhase.AFTER_TEST_METHOD);
} catch (RuntimeException ex) {
resultingException = ex;
log.warn("Swallowed exception to continue with the afterTestMethod() execution chain", ex);
}
super.afterTestMethod(testContext);
if (resultingException != null) {
throw resultingException;
}
}
private void executeMessagingOperation(Set<Messaging> messagingAnnotations, TestContext testContext,
Messaging.ExecutionPhase executionPhase, boolean classLevel) {
messagingAnnotations.forEach(messagingAnnotation ->
this.executeMessagingOperation(messagingAnnotation, testContext, executionPhase, classLevel)
);
}
private void executeMessagingOperation(Messaging messagingAnnotation, TestContext testContext,
Messaging.ExecutionPhase executionPhase, boolean classLevel) {
// ...
AwsSnsAttributes snsAttributes = this.initAwsSnsAttributes(messagingAnnotation.sns(), testContext);
TestingInfrastructurePopulator snsPopulator = snsAttributes != null ? new AwsSnsTopicPopulator(snsAttributes) : null;
AwsSqsAttributes sqsAttributes = this.initAwsSqsAttributes(messagingAnnotation.sqs(), testContext);
TestingInfrastructurePopulator sqsPopulator = sqsAttributes != null ? new AwsSqsQueuePopulator(sqsAttributes) : null;
List<TestingInfrastructurePopulator> populators = new ArrayList<>();
CollectionUtils.addIgnoreNull(populators, snsPopulator);
CollectionUtils.addIgnoreNull(populators, sqsPopulator);
if (messagingAnnotation.executionPhase() == Messaging.ExecutionPhase.BEFORE_TEST_METHOD) {
populators.forEach(populator -> populator.populate());
} else {
populators.forEach(populator -> populator.deleteAll());
}
}
// ...
}
- beforeTestMethod() method first follows the TestExecutionListener chain by executing super.beforeTestMethod(…), then executeMessagingOperation(…).
afterTestMethod() instead first calls executeMessagingOperation(…), handles possible exceptions, then follows the TestExecutionListener chain by calling super.afterTestMethod(…) for other listeners to cleanup resources, for instance.
They both execute executeMessagingOperation(…), each passing theBEFORE_TEST_METHOD
andAFTER_TEST_METHOD
execution phases respectively.
- Multiple executeMessagingOperation() overloaded methods:
-
Retrieves and merges @MessagingGroup and @Messaging configuration annotations, prioritizing method-level over class-level configuration as shown in CustomerSubscriberTestcontainersIntegrationTest.
-
A test class or method can have multiple @Messaging annotations, so it loops through the merged @Messaging annotations and calls another overloaded method passing the test execution phase and each @Messaging annotation.
-
Uses metadata from the @Messaging configuration annotation to instantiate SNS and SQS configuration objects and delegates provisioning or tearing down the SNS and SQS infrastructure to populator classes discussed next.
-
4. SQS QUEUE, SNS TOPIC AND SUBSCRIPTION POPULATOR
MessagingTestExecutionListener delegates populating and deleting queues, topics and subscriptions to these classes.
AwsSnsTopicPopulator.java
:
public class AwsSnsTopicPopulator implements TestingInfrastructurePopulator {
private final AwsSnsAttributes snsAttributes;
@Override
public void populate() {
// 1. Create SNS topic
CreateTopicResponse createTopicResponse = this.createSnsTopic();
// 2. Create SQS queue
CreateQueueResponse createQueueResponse = this.createSqsQueue();
// 3. Get SQS attributes to use QueueArn
GetQueueAttributesResponse getQueueAttributesResponse = this.retrieveSqsQueueAttributes(createQueueResponse.queueUrl());
// 4. Subscribe SNS with protocol=sqs, topic-arn with that of 1.topicArn(), and notification-endpoint with that of 3.QueueArn
this.addSnsSubscription(
createTopicResponse.topicArn(),
getQueueAttributesResponse.attributes().get(QueueAttributeName.QUEUE_ARN)
);
}
@Override
public void deleteAll() {
this.deleteSnsTopic();
this.deleteSqsQueue();
}
private CreateTopicResponse createSnsTopic() {
log.info("Creating SNS Topic '{}'", this.snsAttributes.getSnsTopicName());
CreateTopicRequest request = CreateTopicRequest.builder()
.name(this.snsAttributes.getSnsTopicName())
.build();
CreateTopicResponse result = this.snsAttributes.getSnsClient().createTopic(request);
log.info("{}", result);
return result;
}
@SneakyThrows
private CreateQueueResponse createSqsQueue() {
log.info("Creating SQS Queue '{}'", this.snsAttributes.getSqsAttributes().getSqsQueueName());
CreateQueueRequest request = CreateQueueRequest.builder()
.queueName(this.snsAttributes.getSqsAttributes().getSqsQueueName())
.build();
CreateQueueResponse result = this.snsAttributes.getSqsAttributes().getSqsClient().createQueue(request).get();
log.info("{}", result);
return result;
}
@SneakyThrows
private GetQueueAttributesResponse retrieveSqsQueueAttributes(String sqsQueueUrl) {
log.info("Getting SQS Queue Attributes for queueUrl '{}'", sqsQueueUrl);
GetQueueAttributesRequest request = GetQueueAttributesRequest.builder()
.queueUrl(sqsQueueUrl)
.attributeNames(QueueAttributeName.QUEUE_ARN)
.build();
return this.snsAttributes.getSqsAttributes().getSqsClient().getQueueAttributes(request).get();
}
private SubscribeResponse addSnsSubscription(String snsTopicArn, String sqsQueueArn) {
log.info("Subscribing SNS TopicArn '{}' to SQS Notification-Endpoint QueueArn '{}'", snsTopicArn, sqsQueueArn);
SubscribeRequest subscribeRequest = SubscribeRequest.builder()
.protocol("sqs")
.topicArn(snsTopicArn)
.endpoint(sqsQueueArn)
.build();
return this.snsAttributes.getSnsClient().subscribe(subscribeRequest);
}
private void deleteSnsTopic() {
log.info("Finding TopicArn for SNS Topic '{}'", this.snsAttributes.getSnsTopicName());
ListTopicsResponse snsTopics = this.snsAttributes.getSnsClient().listTopics();
Optional<Topic> optSnsTopic = snsTopics.topics().stream()
.filter(sqsTopic -> sqsTopic.topicArn().endsWith(String.format(":%s", this.snsAttributes.getSnsTopicName())))
.findFirst();
optSnsTopic.ifPresentOrElse(
snsTopic -> {
log.info("Deleting SNS Topic[name={}, topicArn={}]", this.snsAttributes.getSnsTopicName(), snsTopic.topicArn());
DeleteTopicRequest request = DeleteTopicRequest.builder()
.topicArn(snsTopic.topicArn())
.build();
this.snsAttributes.getSnsClient().deleteTopic(request);
},
() -> log.info("SNS Topic '{}' not found", this.snsAttributes.getSnsTopicName())
);
}
@SneakyThrows
private void deleteSqsQueue() {
log.info("Finding QueueUrl for SQS Queue '{}'", this.snsAttributes.getSqsAttributes().getSqsQueueName());
ListQueuesResponse sqsQueues = this.snsAttributes.getSqsAttributes().getSqsClient().listQueues().get();
Optional<String> optQueueUrl = sqsQueues.queueUrls().stream()
.filter(queueUrl -> queueUrl.endsWith(String.format("/%s", this.snsAttributes.getSqsAttributes().getSqsQueueName())))
.findFirst();
optQueueUrl.ifPresentOrElse(
sqsQueueUrl -> {
log.info("Deleting SQS Queue[name={}, queryUrl={}]", this.snsAttributes.getSqsAttributes().getSqsQueueName(), sqsQueueUrl);
DeleteQueueRequest request = DeleteQueueRequest.builder()
.queueUrl(sqsQueueUrl)
.build();
this.snsAttributes.getSqsAttributes().getSqsClient().deleteQueue(request);
},
() -> log.info("SQS Queue '{}' not found", this.snsAttributes.getSqsAttributes().getSqsQueueName())
);
}
}
The AwsSqsQueuePopulator class is similar to the AwsSnsTopicPopulator’s SQS provisioning section, so there’s no need to discuss it.
-
populate() method creates an SNS topic, an SQS queue, and an SQS queue subscription to the SNS topic using AWS Java SDK version
2
for the integration tests to use. -
deleteAll() method deletes the SNS and SQS infrastructure for the next test, if any, to start from scratch, from a known state.
Alternatively, it could have simply emptied the queues and topics.
5. TEST CONFIGURATION
src/test/resources/application-integration-test.yml
:
spring:
cloud:
aws:
region:
static: localstack
credentials:
accessKey: testAccessKey
secretKey: testSecretKey
sns:
endpoint: http://localhost:4566/
sqs:
endpoint: http://localhost:4566/
Not much to discuss about these configuration properties.
But it’s worth mentioning these will be updated at runtime via the @DynamicPropertySource annotation, or in the custom Jupiter/JUnit 5 extension.
6. INTEGRATION TEST CLASS (version 1)
Let’s write a Spring Boot SQS subscriber integration test class that uses everything laid out in this post.
CustomerSubscriberTestcontainersIntegrationTest.java
:
@ExtendWith({ SpringExtension.class, OutputCaptureExtension.class })
@SpringBootTest(classes = Application.class)
@ContextConfiguration(classes = { Application.class })
@ActiveProfiles("integration-test")
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, MessagingTestExecutionListener.class })
@MessagingGroup({
// No need to provision SNS (topic, subscription)
@Messaging(
sqs = @Sqs(queueName = "queue-customers-sqs-test", sqsClient = "sqsAsyncClient"),
executionPhase = Messaging.ExecutionPhase.BEFORE_TEST_METHOD
),
@Messaging(
sqs = @Sqs(queueName = "queue-customers-sqs-test"),
executionPhase = Messaging.ExecutionPhase.AFTER_TEST_METHOD
)
})
@Testcontainers
@Slf4j
public class CustomerSubscriberTestcontainersIntegrationTest {
@Container
private static final LocalStackContainer LOCALSTACK = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:4.6"))
.withServices(LocalStackContainer.Service.SNS, LocalStackContainer.Service.SQS)
.withEnv("SQS_ENDPOINT_STRATEGY", "dynamic")
.waitingFor(Wait.forHttp("/_localstack/init")
.forResponsePredicate(response -> response.contains("\"READY\": true"))
.withStartupTimeout(Duration.ofSeconds(20))
);
// ...
@DynamicPropertySource
static void overrideConfiguration(DynamicPropertyRegistry registry) {
registry.add("spring.cloud.aws.region.static", () -> LOCALSTACK.getRegion());
registry.add("spring.cloud.aws.credentials.access-key", () -> LOCALSTACK.getAccessKey());
registry.add("spring.cloud.aws.credentials.secret-key", () -> LOCALSTACK.getSecretKey());
registry.add("spring.cloud.aws.sns.endpoint", () -> LOCALSTACK.getEndpointOverride(LocalStackContainer.Service.SNS));
registry.add("spring.cloud.aws.sqs.endpoint", () -> LOCALSTACK.getEndpointOverride(LocalStackContainer.Service.SQS));
}
@SneakyThrows
@Test
@MessagingGroup({
// Now also provisioning SNS (topic, queue, subscription)
@Messaging(
sns = @Sns(topicName = "topic-customer-creation-events-test", snsClient = "snsClient",
sqs = @Sqs(queueName = "queue-customers-sns-test", sqsClient = "sqsAsyncClient")
),
sqs = @Sqs(queueName = "queue-customers-sqs-test", sqsClient = "sqsAsyncClient"),
executionPhase = Messaging.ExecutionPhase.BEFORE_TEST_METHOD
),
@Messaging(
sns = @Sns(topicName = "topic-customer-creation-events-test",
sqs = @Sqs(queueName = "queue-customers-sns-test")
),
sqs = @Sqs(queueName = "queue-customers-sqs-test"),
executionPhase = Messaging.ExecutionPhase.AFTER_TEST_METHOD
)
})
public void shouldConsumeSnsNewCustomerNotification(CapturedOutput output) {
// Given
String strCustomer = FileUtils.readFileToString(
new File(CustomerSubscriberIntegrationTest.class.getClassLoader()
.getResource("stubs/new-customer-request.json").getFile()
),
"UTF-8");
// When
this.snsTemplate.convertAndSend(this.snsCustomerTopicArn, strCustomer);
// Then
Awaitility.await().atMost(2, TimeUnit.SECONDS).until(() ->
output.getOut().contains(
"Received SNS message payload: NewCustomerEvent(id=3, firstName=Blah_3, lastName=Meh_3, " +
"emailAddress=invalid.3@asimiotech.com, phone=Phone(number=111-111-1111, type=MOBILE), " +
"mailingAddress=Address(street=1 Main St, suite=null, city=Orlando, state=FL, zipcode=32801))"
)
);
// More assertions
}
// ...
}
-
The integration test class uses JUnit
5
, and for simplicity it includes only one test method. -
@ActiveProfiles annotation includes the
integration-test
profile, causing the tests to use the application-integration-test.yml configuration file. -
@TestExecutionListeners annotation includes a MessagingTestExecutionListener, a custom Spring’s TestExecutionListener that is used in conjunction with the @MessagingGroup and @Messaging annotations to setup the AWS messaging infrastructure for each integration test method to use.
-
shouldConsumeSnsNewCustomerNotification() test method includes @MessagingGroup and @Messaging configuration annotations.
MessagingTestExecutionListener supports using the @MessagingGroup and @Messaging annotations at class or method level, prioritizing method-level annotations. - @Testcontainers and @Container annotations handle the discovery, setup, and teardown of Docker containers that integration tests use.
-
Uses the LocalStackContainer enabling SQS and SNS services.
-
Overrides the wait strategy until a request to
/_localstack/init
returns text containing"READY": true
for up to 20 seconds. -
Exposes the LocalStack container port
4566
to a random host port. -
Sets the environment variable
SQS_ENDPOINT_STRATEGY
todynamic
.
It impacts how LocalStack constructs URLs.
When LocalStack creates an SQS queue and returns the URL, it uses the host port instead of the container port for your Spring Boot application to send requests to.
Our integration tests log:
Getting SQS Queue Attributes for queueUrl 'http://localhost:32769/queue/us-east-1/000000000000/queue-customers-sns-test'
Notice how it uses port32769
instead of4566
.
-
- Lastly, @DynamicPropertySource updates Spring configuration properties at runtime, like SNS and SQS endpoints host and port found in application-integration-test.yml.
Provision SNS topics and SQS queues for testing Spring Boot applications with Testcontainers and LocalStack
What if there are multiple integration test classes depending on a LocalStack Docker container?
Using the same @Testcontainers, @Container instantiation approach as CustomerSubscriberTestcontainersIntegrationTest will result in each integration test class spawning its own Docker container, leading to multiple container lifecycles.
The next section provides an optimization approach for reusing Docker containers in your integration test suite.
7. AwsSqsSnsContainerBootstrap JUPITER/JUNIT 5 EXTENSION
Reusing Docker containers in your integration tests results in using resources more efficiently, fewer containers running concurrently, less I/O, less memory overhead, shorter build time, faster build feedback, among other benefits.
Based on a blog post about reusing Testcontainers with JUnit 5 Extension Callbacks in Spring Boot Integration Tests, let’s write a JUnit 5
Extension that handles the LocalStack Docker container lifecycle management and shares it across the test suite.
AwsSqsSnsContainerBootstrapExtension.java
:
public class AwsSqsSnsContainerBootstrapExtension implements BeforeAllCallback {
private static final AtomicBoolean IS_INITIALIZED = new AtomicBoolean(false);
private static final String LOCALSTACK_IMAGE_NAME = "localstack/localstack:4.6";
private static final DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse(LOCALSTACK_IMAGE_NAME);
private LocalStackContainer localStackContainer;
@Override
public void beforeAll(ExtensionContext context) throws Exception {
// Sets up, starts Docker container, system properties
if (IS_INITIALIZED.compareAndSet(false, true)) {
this.localStackContainer = new LocalStackContainer(LOCALSTACK_IMAGE)
.withServices(LocalStackContainer.Service.SNS, LocalStackContainer.Service.SQS)
.withEnv("SQS_ENDPOINT_STRATEGY", "dynamic")
.waitingFor(Wait.forHttp("/_localstack/init")
.forResponsePredicate(response -> response.contains("\"READY\": true"))
.withStartupTimeout(Duration.ofSeconds(20))
);
this.localStackContainer.start();
this.addSpringCloudAwsProperties();
}
}
private void addSpringCloudAwsProperties() {
// No need for @DynamicPropertySource-spring.cloud.aws.<aws-service>.endpoint, ...
String region = this.localStackContainer.getRegion();
String snsEndpointOverride = this.localStackContainer.getEndpointOverride(LocalStackContainer.Service.SNS).toString();
String sqsEndpointOverride = this.localStackContainer.getEndpointOverride(LocalStackContainer.Service.SQS).toString();
log.info("Overriding AWS config properties [region={} snsEndpoint={}, sqsEndpoint={}]",
region, snsEndpointOverride, sqsEndpointOverride
);
System.setProperty("spring.cloud.aws.region.static", region);
System.setProperty("spring.cloud.aws.sns.endpoint", snsEndpointOverride);
System.setProperty("spring.cloud.aws.sqs.endpoint", sqsEndpointOverride);
}
}
The JUnit 5
BeforeAllCallback.beforeAll() method implementation relies on the IS_INITIALIZED
AtomicBoolean flag to ensure the if
block executes only once.
This guarantees setting up and starting the LocalStack Docker container and updating spring.cloud.aws.* test properties a single time across all tests.
8. INTEGRATION TEST CLASS (version 2)
Let’s now use this extension in our integration test classes.
CustomerSubscriberTestcontainersJUnit5ExtensionIntegrationTest.java
:
-@ExtendWith({ SpringExtension.class, OutputCaptureExtension.class })
+@ExtendWith({ AwsSqsSnsContainerBootstrapExtension.class, SpringExtension.class, OutputCaptureExtension.class })
// ...
-@Testcontainers
public class CustomerSubscriberTestcontainersJUnit5ExtensionIntegrationTest {
- @Container
- private static final LocalStackContainer LOCALSTACK = new LocalStackContainer(
- DockerImageName.parse("localstack/localstack:4.6"))
- .withServices(LocalStackContainer.Service.SNS, LocalStackContainer.Service.SQS)
- .withEnv("SQS_ENDPOINT_STRATEGY", "dynamic")
- .waitingFor(Wait.forHttp("/_localstack/init")
- .forResponsePredicate(response -> response.contains("\"READY\": true"))
- .withStartupTimeout(Duration.ofSeconds(20))
- );
- @DynamicPropertySource
- static void overrideConfiguration(DynamicPropertyRegistry registry) {
- registry.add("spring.cloud.aws.region.static", () -> LOCALSTACK.getRegion());
- registry.add("spring.cloud.aws.credentials.access-key", () -> LOCALSTACK.getAccessKey());
- registry.add("spring.cloud.aws.credentials.secret-key", () -> LOCALSTACK.getSecretKey());
- registry.add("spring.cloud.aws.sns.endpoint", () -> LOCALSTACK.getEndpointOverride(LocalStackContainer.Service.SNS));
- registry.add("spring.cloud.aws.sqs.endpoint", () -> LOCALSTACK.getEndpointOverride(LocalStackContainer.Service.SQS));
- }
// ...
}
We added the new JUnit 5
Extension AwsSqsSnsContainerBootstrapExtension.class to the @ExtendWith annotation, and this allows us to eliminate the previous @Testcontainers, @Container instantiation, and @DynamicPropertySource usage for runtime Spring property updates.
Let’s run the two integration test classes that use the new extension from command line:
mvn -Dtest=CustomerSubscriberTestcontainersJUnit5ExtensionIntegrationTest,DuplicateCustomerSubscriberTestcontainersJUnit5ExtensionIntegrationTest verify
...
13:09:05.543 [main] INFO com.asimiotech.demo.test.testcontainers.AwsSqsSnsContainerBootstrapExtension -- Overriding AWS config properties [region=us-east-1 snsEndpoint=http://192.168.1.188:32769, sqsEndpoint=http://192.168.1.188:32769]
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.54 s -- in com.asimiotech.demo.messaging.customer.CustomerSubscriberTestcontainersJUnit5ExtensionIntegrationTest
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.126 s -- in com.asimiotech.demo.messaging.customer.DuplicateCustomerSubscriberTestcontainersJUnit5ExtensionIntegrationTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
...
The test suite with four tests now starts a single LocalStack container, which could be confirmed with the log Overriding AWS config properties [region=us-east-1 snsEndpoint=
.
9. CONCLUSION
This blog post covered writing integration tests for Spring Boot, SQS, SNS applications with Testcontainers, LocalStack and Docker.
It helped you provision the SQS queues and SNS topics for the Spring Boot integration tests to publish to, and to consume from.
It also included a custom Spring’s TestExecutionListener that hooks into the integration test life cycle to set up and tear down the SQS and SNS infrastructure before and after each Spring Boot integration test.
Finally, you’ve also learned how to create a custom JUnit 5
Extension that enables sharing a Testcontainers LocalStack instance across all your SNS and SQS integration tests, rather than having each test class spin up a separate LocalStack Docker container.
Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.
10. SOURCE CODE
Your organization can now save time and costs by purchasing a working code base, clean implementation, with support for future revisions.
NEED HELP?
I provide Consulting Services.ABOUT THE AUTHOR
