1. 概述

在这个教程中,我们将创建一个简单的Spring应用,它连接到ActiveMQ来发送和接收消息。我们将专注于测试这个应用以及测试Spring JMS的整体方法。

2. 应用设置

首先,让我们创建一个基础的应用,用于测试。我们需要添加必要的依赖并实现消息处理功能。

2.1. 依赖

在我们的项目pom.xml中添加所需的依赖。为了能够监听JMS消息,我们需要[Spring JMS](https://mvnrepository.com/artifact/org.springframework/spring-jms/4.3.4.RELEASE/jar)。我们将使用[ActiveMQ-Junit](https://mvnrepository.com/artifact/org.apache.activemq.tooling/activemq-junit/5.17.1/jar)启动嵌入式ActiveMQ实例,用于部分测试,而使用[TestContainers](https://mvnrepository.com/artifact/org.testcontainers/testcontainers/1.17.3/jar)在其他测试中运行一个ActiveMQ Docker容器:

<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. 应用代码

现在让我们创建一个能监听消息的Spring应用:

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

我们需要创建一个配置类,并通过使用[@EnableJms]注解启用JMS,配置ConnectionFactory以连接到我们的ActiveMQ实例

@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());
    }
}

接下来,让我们创建一个可以接收和处理消息的监听器:

@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());
    }
}

我们还需要一个类来发送消息:

@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. 使用嵌入式ActiveMQ进行测试

现在来测试我们的应用。我们首先使用嵌入式ActiveMQ实例。让我们创建测试类,并添加一个JUnit规则来管理我们的ActiveMQ实例:

@RunWith(SpringRunner.class)
public class EmbeddedActiveMqTests4 {

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

    @Test
    public void 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

在测试类中的所有测试执行完毕后,代理将被停止。

我们需要配置应用连接到这个ActiveMQ实例,以便我们能正确测试MessageListenerMessageSender类:

@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());
    }
}

这个类使用了一个特殊的ConnectionFactory,它从我们的嵌入式代理获取URL。现在我们需要使用这个配置,通过在包含测试的类上添加[@ContextConfiguration]注解:

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

3.1. 发送消息

让我们编写第一个测试,检查MessageSender类的功能。首先,我们可以通过字段注入的方式获取到这个类的一个实例:

@Autowired
private MessageSender messageSender;

让我们向ActiveMQ发送一个简单消息,并添加一些断言来检查功能:

@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());
}

现在我们确信MessageSender工作正常,因为发送消息后,队列中恰好有一个包含正确文本的条目。

3.2. 接收消息

我们也来检查Listener类。首先,我们在新的测试方法中发送一条消息,使用嵌入式代理。我们的监听器的目标是“queue-1”,因此我们需要确保这里使用的是相同的名字。

我们将使用Mockito来检查监听器的行为。我们将使用[@SpyBean]注解获取MessageListener的实例:

@SpyBean
private MessageListener messageListener;

然后,我们将检查方法是否被调用,并使用[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());
}

现在我们运行测试,两者都通过了。

4. 使用TestContainers进行测试

让我们看看另一种测试Spring应用中JMS的方法。我们可以使用TestContainers来运行一个ActiveMQ Docker容器并在测试中连接到它。

创建一个新的测试类,并包含Docker容器作为JUnit规则:

@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 {
    }
}

运行这个测试并查看日志。我们可以看到TestContainers的一些信息,因为它正在拉取指定的docker镜像,并启动容器:

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

让我们创建一个类似于我们使用ActiveMQ时实现的配置类。唯一的区别是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. 发送消息

现在我们来测试MessageSender类,看看它是否能与这个Docker容器一起工作。这次我们不能使用对EmbeddedBroker的方法,但Spring的JmsTemplate也很容易使用:

@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());
}

我们可以使用JmsTemplate读取队列的内容,检查我们的类是否发送了正确的消息。

4.2. 接收消息

测试我们的监听器类也没有太大的不同。让我们使用JmsTemplate发送一条消息,并验证我们的监听器是否收到了正确的文本:

@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. 总结

在这篇文章中,我们创建了一个基本的应用,它使用Spring JMS发送和接收消息。然后,我们讨论了两种测试它的方法。

首先,我们使用了一个嵌入式ActiveMQ实例,它甚至提供了一些方便的方法与代理交互。其次,我们使用TestContainers在一个模拟真实场景的Docker容器中测试我们的代码。

如往常一样,这些示例的源代码可以在GitHub上找到。