Partner – Microsoft – NPI (cat= Spring)
announcement - icon

Azure Spring Apps is a fully managed service from Microsoft (built in collaboration with VMware), focused on building and deploying Spring Boot applications on Azure Cloud without worrying about Kubernetes.

And, the Enterprise plan comes with some interesting features, such as commercial Spring runtime support, a 99.95% SLA and some deep discounts (up to 47%) when you are ready for production.

>> Learn more and deploy your first Spring Boot app to Azure.

You can also ask questions and leave feedback on the Azure Spring Apps GitHub page.

1. Overview

In this tutorial, we’ll create a simple Spring application that connects to ActiveMQ to send and receive messages. We’ll focus on testing this application and the different approaches to test Spring JMS overall.

2. Application Setup

First, let’s create a basic application that can be used for testing. We’ll need to add the necessary dependencies and implement the message handling.

2.1. Dependencies

Let’s add the required dependencies to our project’s pom.xml. We need Spring JMS to be able to listen to JMS messages. We’ll use ActiveMQ-Junit to start an embedded ActiveMQ instance for a part of the tests and TestContainers to run an ActiveMQ Docker container in the other tests:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jms</artifactId>
    <version>4.3.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.apache.activemq.tooling</groupId>
    <artifactId>activemq-junit</artifactId>
    <version>5.16.5</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.17.3</version>
    <scope>test</scope>
</dependency>

2.2. Application Code

Now let’s create a Spring application that can listen for messages:

@ComponentScan
public class JmsApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(JmsApplication.class);
    }
}

We need to create a configuration class and enable JMS with the @EnableJms annotation and configure the ConnectionFactory to connect to our ActiveMQ instance:

@Configuration
@EnableJms
public class JmsConfig {

    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        return factory;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        return new ActiveMQConnectionFactory("tcp://localhost:61616");
    }

    @Bean
    public JmsTemplate jmsTemplate() {
        return new JmsTemplate(connectionFactory());
    }
}

After this, let’s create our listener that can receive and process messages:

@Component
public class MessageListener {

    private static final Logger logger = LoggerFactory.getLogger(MessageListener.class);

    @JmsListener(destination = "queue-1")
    public void sampleJmsListenerMethod(TextMessage message) throws JMSException {
        logger.info("JMS listener received text message: {}", message.getText());
    }
}

We’ll also need a class that can send messages:

@Component
public class MessageSender {

    @Autowired
    private JmsTemplate jmsTemplate;

    private static final Logger logger = LoggerFactory.getLogger(MessageSender.class);

    public void sendTextMessage(String destination, String message) {
        logger.info("Sending message to {} destination with text {}", destination, message);
        jmsTemplate.send(destination, s -> s.createTextMessage(message));
    }
}

3. Testing With Embedded ActiveMQ

Let’s test our application. We’ll use an embedded ActiveMQ instance first. Let’s create our test class and add a JUnit Rule that manages our ActiveMQ instance:

@RunWith(SpringRunner.class)
public class EmbeddedActiveMqTests4 {

    @ClassRule
    public static EmbeddedActiveMQBroker embeddedBroker = new EmbeddedActiveMQBroker();

    @Test
    public void test() {
    }

    // ...
}

Let’s run this empty test and inspect the logs. We can see that an embedded broker is started with our test:

INFO | Starting embedded ActiveMQ broker: embedded-broker
INFO | Using Persistence Adapter: MemoryPersistenceAdapter
INFO | Apache ActiveMQ 5.14.1 (embedded-broker, ID:DESKTOP-52539-254421135-0:1) is starting
INFO | Apache ActiveMQ 5.14.1 (embedded-broker, ID:DESKTOP-52539-254421135-0:1) started
INFO | For help or more information please see: http://activemq.apache.org
INFO | Connector vm://embedded-broker started
INFO | Successfully connected to vm://embedded-broker?create=false

The broker is stopped after all the tests are executed in our test class.

We need to configure our application to connect to this ActiveMQ instance so that we can test our MessageListener and MessageSender classes properly:

@Configuration
@EnableJms
static class TestConfiguration {
    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        return factory;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        return new ActiveMQConnectionFactory(embeddedBroker.getVmURL());
    }

    @Bean
    public JmsTemplate jmsTemplate() {
        return new JmsTemplate(connectionFactory());
    }
}

This class uses a special ConnectionFactory which gets the URL from our embedded broker. Now we need to use this configuration by adding the @ContextConfiguration annotation to the class containing our tests:

@ContextConfiguration(classes = { TestConfiguration.class, MessageSender.class }) public class EmbeddedActiveMqTests {

3.1. Sending Messages

Let’s write our first test and check the functionality of our MessageSender class. First, we need to get a reference to an instance of this class by simply injecting it as a field:

@Autowired
private MessageSender messageSender;

Let’s send a simple message to ActiveMQ and add some assertions to check the functionality:

@Test
public void whenSendingMessage_thenCorrectQueueAndMessageText() throws JMSException {
    String queueName = "queue-2";
    String messageText = "Test message";

    messageSender.sendTextMessage(queueName, messageText);

    assertEquals(1, embeddedBroker.getMessageCount(queueName));
    TextMessage sentMessage = embeddedBroker.peekTextMessage(queueName);
    assertEquals(messageText, sentMessage.getText());
}

Now we are sure that our MessageSender works properly because the queue contains exactly one entry with the correct text after we send the message.

3.2. Receiving Messages

Let’s check our listener class as well. Let’s start with creating a new test method and sending a message with the embedded broker. Our listener is set to use “queue-1” as its destination, so we need to ensure that we are using the same name here.

Let’s use Mockito to check the listener’s behavior. We’ll use the @SpyBean annotation to get an instance of the MessageListener:

@SpyBean
private MessageListener messageListener;

Then, we’ll check if the method was called and capture the received method argument with an ArgumentCaptor:

@Test
public void whenListening_thenReceivingCorrectMessage() throws JMSException {
    String queueName = "queue-1";
    String messageText = "Test message";

    assertEquals(0, embeddedBroker.getDestination(queueName).getDestinationStatistics().getDispatched().getCount());
    assertEquals(0, embeddedBroker.getDestination(queueName).getDestinationStatistics().getMessages().getCount());

    embeddedBroker.pushMessage(queueName, messageText);

    ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(TextMessage.class);
    Mockito.verify(messageListener, Mockito.timeout(100))
        .sampleJmsListenerMethod(messageCaptor.capture());

    TextMessage receivedMessage = messageCaptor.getValue();
    assertEquals(messageText, receivedMessage.getText());

    assertEquals(1, embeddedBroker.getDestination(queueName).getDestinationStatistics().getDispatched().getCount());
    assertEquals(0, embeddedBroker.getDestination(queueName).getDestinationStatistics().getMessages().getCount());
}

We can now run the tests, and both of them pass.

4. Testing With TestContainers

Let’s see another approach for testing JMS in Spring applications. We can use TestContainers to run an ActiveMQ Docker container and connect to it in our tests.

Let’s create a new test class and include the Docker container as a JUnit Rule:

@RunWith(SpringRunner.class)
public class TestContainersActiveMqTests {

    @ClassRule
    public static GenericContainer<?> activeMqContainer 
      = new GenericContainer<>(DockerImageName.parse("rmohr/activemq:5.14.3")).withExposedPorts(61616);

    @Test
    public void test() throws JMSException {
    }
}

Let’s run this test and check the logs. We can see some information related to TestContainers as it is pulling the specified docker image and as it starts the container:

INFO | Creating container for image: rmohr/activemq:5.14.3
INFO | Container rmohr/activemq:5.14.3 is starting: e9b0ddcd45c54fc9994aff99d734d84b5fae14b55fdc70887c4a2c2309b229a7
INFO | Container rmohr/activemq:5.14.3 started in PT2.635S

Let’s create a configuration class similar to what we implemented with ActiveMQ. The only difference is the configuration of the ConnectionFactory:

@Bean
public ConnectionFactory connectionFactory() {
    String brokerUrlFormat = "tcp://%s:%d";
    String brokerUrl = String.format(brokerUrlFormat, activeMqContainer.getHost(), activeMqContainer.getFirstMappedPort());
    return new ActiveMQConnectionFactory(brokerUrl);
}

4.1. Sending Messages

Let’s test our MessageSender class and see if it works with this Docker container. This time we can’t use the methods on the EmbeddedBroker, but the Spring JmsTemplate is easy to use too:

@Autowired
private MessageSender messageSender;

@Autowired
private JmsTemplate jmsTemplate;

@Test
public void whenSendingMessage_thenCorrectQueueAndMessageText() throws JMSException {
    String queueName = "queue-2";
    String messageText = "Test message";

    messageSender.sendTextMessage(queueName, messageText);

    Message sentMessage = jmsTemplate.receive(queueName);
    Assertions.assertThat(sentMessage).isInstanceOf(TextMessage.class);

    assertEquals(messageText, ((TextMessage) sentMessage).getText());
}

We can use the JmsTemplate to read the contents of the queue and check if our class sent the correct message.

4.2. Receiving Messages

Testing our listener class isn’t much different either. Let’s send a message using the JmsTemplate and verify if our listener received the correct text:

@SpyBean
private MessageListener messageListener;

@Test
public void whenListening_thenReceivingCorrectMessage() throws JMSException {
    String queueName = "queue-1";
    String messageText = "Test message";

    jmsTemplate.send(queueName, s -> s.createTextMessage(messageText));

    ArgumentCaptor<TextMessage> messageCaptor = ArgumentCaptor.forClass(TextMessage.class);

    Mockito.verify(messageListener, Mockito.timeout(100)).sampleJmsListenerMethod(messageCaptor.capture());

    TextMessage receivedMessage = messageCaptor.getValue();
    assertEquals(messageText, receivedMessage.getText());
}

5. Conclusion

In this article, we created a basic application that can send and receive messages with Spring JMS. Then, we discussed two ways to test it.

Firstly, we used an embedded ActiveMQ instance that even provides some convenient methods to interact with the broker. Secondly, we used TestContainers to test our code with a docker container that simulates real-world scenarios better.

As always, the source code for these examples is available over on GitHub.

Course – LS (cat=Spring)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.