Testing Spring JMS with Testcontainers and ActiveMQ Artemis
Purpose
When I first started testing JMS applications, I relied heavily on mocks. Mock a JmsTemplate, verify convertAndSend() was called, and move on. It felt clean and fast. But then I encountered issues that mocks couldn’t catch: connection pooling problems, message serialization edge cases, and subtle configuration mismatches between my test environment and production.
The reality is that messaging systems have complex behaviors that mocks simply cannot replicate. Message ordering, redelivery policies, dead letter queues, and broker-specific quirks all affect how your application behaves in production. Testing with a real broker gives me confidence that my application will work correctly when deployed.
This is where Testcontainers comes in. It spins up a real ActiveMQ Artemis broker in a Docker container for my tests, providing all the benefits of integration testing without the complexity of maintaining a separate test broker infrastructure.
Setting Up Test Dependencies
First, I need to add the required dependencies to my Maven project:
<dependencies> <!-- Spring Boot Starter for JMS --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-artemis</artifactId> </dependency>
<!-- Testcontainers Artemis Module --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>activemq</artifactId> <version>1.19.3</version> <scope>test</scope> </dependency>
<!-- Spring Boot Test Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<!-- Awaitility for async assertions --> <dependency> <groupId>org.awaitility</groupId> <artifactId>awaitility</artifactId> <version>4.2.0</version> <scope>test</scope> </dependency></dependencies>The activemq dependency from Testcontainers provides the ArtemisContainer class, which handles all the Docker container management for me.
Creating the Test Class
Here’s a complete test class that demonstrates the integration with Testcontainers:
package com.example;
import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.jms.core.JmsTemplate;import org.testcontainers.activemq.ArtemisContainer;import org.springframework.test.context.DynamicPropertyRegistry;import org.springframework.test.context.DynamicPropertySource;import org.junit.jupiter.api.BeforeEach;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTestclass JmsIntegrationTest {
// The container must be static for Spring Boot's context caching @SuppressWarnings("resource") static ArtemisContainer artemisContainer = new ArtemisContainer("apache/activemq-artemis:2.31.0") .withUser("testuser") .withPassword("testpass");
static { artemisContainer.start(); }
// Inject container properties into Spring Boot's configuration @DynamicPropertySource static void configureBroker(DynamicPropertyRegistry registry) { registry.add("spring.artemis.broker-url", artemisContainer::getBrokerUrl); registry.add("spring.artemis.user", () -> "testuser"); registry.add("spring.artemis.password", () -> "testpass"); }
@Autowired private JmsTemplate jmsTemplate;
@Autowired private TestMessageListener testMessageListener;
private static final String TEST_QUEUE = "test.queue";
@Test void shouldSendAndReceiveMessage() { // Arrange String messageContent = "Hello, Testcontainers!";
// Act jmsTemplate.convertAndSend(TEST_QUEUE, messageContent);
// Assert - use Awaitility for async assertions await() .atMost(5, TimeUnit.SECONDS) .pollInterval(100, TimeUnit.MILLISECONDS) .untilAsserted(() -> { List<String> received = testMessageListener.getReceivedMessages(); assertEquals(1, received.size()); assertEquals(messageContent, received.get(0)); }); }}Several important details are worth noting here:
-
Static container: The
ArtemisContainermust bestaticbecause Spring Boot caches the application context between tests, and we want the container to persist across test runs for efficiency. -
@DynamicPropertySource: This annotation allows me to inject the container’s dynamic properties (like the broker URL) into Spring Boot’s configuration at runtime. -
Awaitility for assertions: Message delivery is asynchronous, so I use Awaitility instead of
Thread.sleep(). This approach is more reliable and provides better error messages when tests fail.
Creating a Test Message Listener
To capture messages in my tests, I create a simple listener that stores received messages in a thread-safe list:
package com.example;
import org.springframework.jms.annotation.JmsListener;import org.springframework.stereotype.Component;import java.util.List;import java.util.concurrent.CopyOnWriteArrayList;
@Componentpublic class TestMessageListener {
private final List<String> receivedMessages = new CopyOnWriteArrayList<>();
@JmsListener(destination = "test.queue") public void receiveMessage(String message) { receivedMessages.add(message); }
public List<String> getReceivedMessages() { return new ArrayList<>(receivedMessages); }
public void clear() { receivedMessages.clear(); }}I use CopyOnWriteArrayList for thread-safety, since the listener runs in a separate thread from the test.
Running the Tests
When I run the tests, Testcontainers automatically pulls the Docker image (if not already present), starts the Artemis broker, and makes it available for my tests:
$ mvn test
[INFO] Running JmsIntegrationTest2026-03-26 10:00:00 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Found Docker client2026-03-26 10:00:01 [main] INFO org.testcontainers.DockerClientFactory - Docker host IP address is localhost2026-03-26 10:00:01 [main] INFO org.testcontainers.containers.GenericContainer - Creating container with image: apache/activemq-artemis:2.31.0...[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0[INFO] BUILD SUCCESSThe container starts before Spring Boot initializes, so by the time my test runs, the broker is ready to accept connections.
Common Mistakes to Avoid
Over time, I’ve learned to avoid these pitfalls when testing JMS with Testcontainers:
Using Thread.sleep() instead of Awaitility: This is the most common mistake I see. Thread.sleep(1000) might work on your machine, but it makes tests slow and flaky. Awaitility waits exactly as long as needed, making tests both faster and more reliable.
Forgetting to make @Container static: In Spring Boot tests, if the container is not static, a new container starts for each test method, which is slow and wasteful. Making it static allows Spring to cache the context and reuse the container.
Skipping broker credentials: The container requires authentication. When I forget to configure the user and password, my tests fail with authentication errors.
Only asserting message arrival: It’s tempting to just check that a message arrived. But I’ve caught bugs by asserting the actual content of messages, not just their existence.
Testing Message Flow End-to-End
For a more complete test, I often verify the entire message pipeline:
package com.example;
import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.jms.core.JmsTemplate;import org.testcontainers.activemq.ArtemisContainer;import org.springframework.test.context.DynamicPropertyRegistry;import org.springframework.test.context.DynamicPropertySource;import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;import static org.junit.jupiter.api.Assertions.*;
@SpringBootTestclass EndToEndJmsTest {
static ArtemisContainer artemisContainer = new ArtemisContainer("apache/activemq-artemis:2.31.0") .withUser("testuser") .withPassword("testpass");
static { artemisContainer.start(); }
@DynamicPropertySource static void configureBroker(DynamicPropertyRegistry registry) { registry.add("spring.artemis.broker-url", artemisContainer::getBrokerUrl); registry.add("spring.artemis.user", () -> "testuser"); registry.add("spring.artemis.password", () -> "testpass"); }
@Autowired private JmsTemplate jmsTemplate;
@Autowired private TestMessageListener testMessageListener;
@Test void shouldProcessOrderMessageEndToEnd() { // Arrange String orderMessage = """ {"orderId":"ORD-123","productId":"PROD-456","quantity":2} """; testMessageListener.clear();
// Act jmsTemplate.convertAndSend("orders.queue", orderMessage);
// Assert - verify complete message processing await() .atMost(10, TimeUnit.SECONDS) .pollInterval(200, TimeUnit.MILLISECONDS) .untilAsserted(() -> { var messages = testMessageListener.getReceivedMessages(); assertFalse(messages.isEmpty()); assertTrue(messages.get(0).contains("ORD-123")); assertTrue(messages.get(0).contains("PROD-456")); }); }}This approach gives me confidence that my entire message flow works correctly, from sending to processing.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments