Skip to content

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:

pom.xml
<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:

src/test/java/com/example/JmsIntegrationTest.java
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;
@SpringBootTest
class 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:

  1. Static container: The ArtemisContainer must be static because Spring Boot caches the application context between tests, and we want the container to persist across test runs for efficiency.

  2. @DynamicPropertySource: This annotation allows me to inject the container’s dynamic properties (like the broker URL) into Spring Boot’s configuration at runtime.

  3. 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:

src/test/java/com/example/TestMessageListener.java
package com.example;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Component
public 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:

Running tests
$ mvn test
[INFO] Running JmsIntegrationTest
2026-03-26 10:00:00 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Found Docker client
2026-03-26 10:00:01 [main] INFO org.testcontainers.DockerClientFactory - Docker host IP address is localhost
2026-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 SUCCESS

The 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:

src/test/java/com/example/EndToEndJmsTest.java
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.*;
@SpringBootTest
class 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