Статьи

Обманка инфраструктуры JMS с помощью MockRunner в пользу тестирования

В этой статье показан * один * способ макетирования инфраструктуры JMS в приложении Spring JMS. Это позволяет нам тестировать нашу инфраструктуру JMS без необходимости зависеть от наличия физического соединения. Если вы читаете эту статью, есть вероятность, что вы также разочарованы неудачными тестами в среде непрерывной интеграции из-за недоступности (временно) сервера JMS. Посмеиваясь над JMS-провайдером, разработчики могут свободно тестировать не только функциональность своего API (модульные тесты), но и параметры различных компонентов, например, в контейнере Spring.

В этой статье я покажу, как можно полностью протестировать приложение Spring JMS Hello World без физического соединения с JMS. Я хотел бы подчеркнуть тот факт, что код в этой статье никоим образом не предназначен для производства и что показанный подход является лишь одним из многих.

Инфраструктура

Для этой статьи я использую следующую инфраструктуру:

  • Apache ActiveMQ , поставщик JMS с открытым исходным кодом, работающий на установке Ubuntu
  • Весна 3
  • Java 6
  • MockRunner
  • Eclipse как среда разработки, работающая на Windows 7

Конфигурация Spring

Я уверен, что использование того, что я определяю как шаблон конфигурации Spring (SCSP), является правильным решением почти во всех случаях, когда требуется надежная инфраструктура тестирования. Я посвятим всю статью SCSP, а пока это выглядит так:

Контекст приложения Spring

Здесь следует содержание jemosJms-appContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-3.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">   
   
    <bean id="helloWorldConsumer" class="uk.co.jemos.experiments.HelloWorldHandler" />
   
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
      <property name="connectionFactory" ref="jmsConnectionFactory" />
    </bean>

    <jms:listener-container connection-factory="jmsConnectionFactory" >
        <jms:listener destination="jemos.tests" ref="helloWorldConsumer" method="handleHelloWorld" />
    </jms:listener-container>

</beans>

 

Единственная важная вещь, которую следует здесь отметить, это то, что есть некоторые сервисы, которые полагаются на существующий компонент с именем jmsConnectionFactory, но такой компонент не определен в этом файле. Это ключ к SCSP, и я проиллюстрирую это в одной из моих будущих статей.

Реализация контекста приложения Spring

Ниже следует содержимое файла jemosJms-appContextImpl.xml, который можно рассматривать как реализацию контекста приложения Spring, определенного выше.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-3.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">

    <import resource="classpath:jemosJms-appContext.xml" />

    <bean id="jmsConnectionFactory" class="org.apache.activemq.spring.ActiveMQConnectionFactory">
      <property name="brokerURL" value="tcp://myJmsServer:61616" />
    </bean>

</beans>

 

Этот контекстный файл Spring импортирует определенный выше контекст приложения Spring, и именно этот контекст приложения объявил фабрику соединений.

Такое отделение требования bean-компонента (в супер-контексте) от его фактического объявления (реализация контекста приложения Spring) представляет собой базовое хранилище SCSP.

Насмешка над JMS-провайдером — контекст приложения Spring Test и MockRunner

Следуя тому же подходу, который я использовал выше, теперь я могу объявить фиктивную фабрику соединений, которая не требует физического соединения с провайдером JMS. Здесь следует содержание jemosJmsTest-appContext.xml. Обратите внимание, что этот файл должен находиться в тестовых ресурсах вашего проекта, то есть он никогда не должен попадать в производство.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-3.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">

    <import resource="classpath:jemosJms-appContext.xml" />

    <bean id="destinationManager" class="com.mockrunner.jms.DestinationManager"/>
    <bean id="configurationManager" class="com.mockrunner.jms.ConfigurationManager"/>


    <bean id="jmsConnectionFactory" class="com.mockrunner.mock.jms.MockQueueConnectionFactory" >
        <constructor-arg index="0" ref="destinationManager" />
        <constructor-arg index="1" ref="configurationManager" />
    </bean>

</beans>

Здесь файл контекста тестового приложения Spring импортирует контекст приложения Spring (не его реализацию) и объявляет фиктивную фабрику соединений, благодаря классу MockRunner MockQueueConnectionFactory.

Слушатель POJO

Работа по обработке сообщения делегируется простому POJO, который также объявляется как bean-компонент:

package uk.co.jemos.experiments;

public class HelloWorldHandler {

    /** The application logger */
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(HelloWorldHandler.class);

    public void handleHelloWorld(String msg) {

        LOG.info("Received message: " + msg);

    }

}

 

В этом классе нет ничего очаровательного. В реальной жизни это, вероятно, должна быть реализация интерфейса, но здесь я хотел, чтобы все было просто.

Простой производитель сообщений JMS

Ниже приведен пример производителя сообщений JMS, который будет использовать реальную инфраструктуру JMS для отправки сообщений:

package uk.co.jemos.experiments;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jms.core.JmsTemplate;

public class JmsTest {

    /** The application logger */
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(JmsTest.class);

    /**
     * @param args
     */
    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext(
                "classpath:jemosJms-appContextImpl.xml");

        JmsTemplate jmsTemplate = ctx.getBean(JmsTemplate.class);

        jmsTemplate.send("jemos.tests", new HelloWorldMessageCreator());

        LOG.info("Message sent successfully");

    }
   
}

 

Единственное, что здесь интересует, — это то, что этот класс извлекает реальный JmsTemplate для отправки сообщения в очередь.

Теперь, если бы я запускал этот класс как есть, я бы получил следующее:

2011-07-31 17:09:46 ClassPathXmlApplicationContext [INFO] Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@19e0ff2f: startup date [Sun Jul 31 17:09:46 BST 2011]; root of context hierarchy
2011-07-31 17:09:46 XmlBeanDefinitionReader [INFO] Loading XML bean definitions from class path resource [jemosJms-appContextImpl.xml]
2011-07-31 17:09:46 XmlBeanDefinitionReader [INFO] Loading XML bean definitions from class path resource [jemosJms-appContext.xml]
2011-07-31 17:09:46 DefaultListableBeanFactory [INFO] Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@3479e304: defining beans [helloWorldConsumer,jmsTemplate,org.springframework.jms.listener.DefaultMessageListenerContainer#0,jmsConnectionFactory]; root of factory hierarchy
2011-07-31 17:09:46 DefaultLifecycleProcessor [INFO] Starting beans in phase 2147483647
2011-07-31 17:09:47 HelloWorldHandler [INFO] Received message: Hello World
2011-07-31 17:09:47 JmsTest [INFO] Message sent successfully

 

Написание интеграционного теста

Существуют различные толкования того, что означают разные типы тестов, и я не претендую на то, чтобы получить единственный ответ; Моя интерпретация состоит в том, что интеграционный тест — это функциональный тест, который также соединяет различные компоненты вместе, но который не взаимодействует с реальной внешней инфраструктурой (например, тест интеграции Dao подделывает данные, тест интеграции JMS подделывает физическое соединение JMS, тест интеграции HTTP подделки удаленного веб-хоста и т. д.). В то время как, по моему мнению, основная цель модульного (функционального) теста состоит в том, чтобы позволить API выйти из тестов, основная цель интеграционного теста состоит в том, чтобы проверить, работает ли сантехника между компонентами так, как ожидается, чтобы избежать неожиданностей в производственная среда.

И модульные (функциональные), и интеграционные тесты должны выполняться очень быстро (например, менее 10 минут), поскольку они составляют то, что можно считать «маркером разработки». Если модульные и интеграционные тесты зеленые, нужно быть уверенным в том, что 90% функциональных возможностей работают как положено; в моих проектах, когда юнит-тесты и тесты интеграции имеют зеленый цвет, я позволяю разработчикам бесплатно выпустить токен. Это не означает, что остальные 10% (например, взаимодействие с реальной инфраструктурой) не должны тестироваться, но это может быть делегировано системным тестам, которые выполняются ночью и не требуют токена разработки. Поскольку модульные и интеграционные тесты должны выполняться быстро, взаимодействие с внешней инфраструктурой должно быть по возможности смоделировано.

Далее следует интеграционный тест для обработчика Hello World:

package uk.co.jemos.experiments.test.integration;

import javax.annotation.Resource;
import javax.jms.TextMessage;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;

import uk.co.jemos.experiments.HelloWorldHandler;
import uk.co.jemos.experiments.HelloWorldMessageCreator;

import com.mockrunner.jms.DestinationManager;
import com.mockrunner.mock.jms.MockQueue;


/**
 * @author mtedone
 *
 */
@ContextConfiguration(locations = { "classpath:jemosJmsTest-appContextImpl.xml" })
public class HelloWorldHandlerIntegrationTest extends AbstractJUnit4SpringContextTests {

    @Resource
    private JmsTemplate jmsTemplate;

    @Resource
    private DestinationManager mockDestinationManager;

    @Resource
    private HelloWorldHandler helloWorldHandler;

    @Before
    public void init() {
        Assert.assertNotNull(jmsTemplate);
        Assert.assertNotNull(mockDestinationManager);
        Assert.assertNotNull(helloWorldHandler);
    }

    @Test
    public void helloWorld() throws Exception {
        MockQueue mockQueue = mockDestinationManager.createQueue("jemos.tests");

        jmsTemplate.send(mockQueue, new HelloWorldMessageCreator());

        TextMessage message = (TextMessage) jmsTemplate.receive(mockQueue);

        Assert.assertNotNull("The text message cannot be null!",
                message.getText());

        helloWorldHandler.handleHelloWorld(message.getText());

    }

}

 

And here follows the output:

2011-07-31 17:17:26 XmlBeanDefinitionReader [INFO] Loading XML bean definitions from class path resource [jemosJmsTest-appContextImpl.xml]
2011-07-31 17:17:26 XmlBeanDefinitionReader [INFO] Loading XML bean definitions from class path resource [jemosJms-appContext.xml]
2011-07-31 17:17:26 GenericApplicationContext [INFO] Refreshing org.springframework.context.support.GenericApplicationContext@f01a1e: 
startup date [Sun Jul 31 17:17:26 BST 2011]; root of context hierarchy 2011-07-31 17:17:27 DefaultListableBeanFactory [INFO] Pre-instantiating singletons in org.springframework.beans.factory.support.
DefaultListableBeanFactory@39478a43: defining beans [helloWorldConsumer,jmsTemplate,org.springframework.jms.listener.DefaultMessageListener
Container#0,destinationManager,configurationManager,jmsConnectionFactory,org.springframework.context.annotation.internalConfigurationAnnotation
Processor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequired
AnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor]; root of factory hierarchy 2011-07-31 17:17:27 DefaultLifecycleProcessor [INFO] Starting beans in phase 2147483647 2011-07-31 17:17:27 HelloWorldHandler [INFO] Received message: Hello World 2011-07-31 17:17:27 GenericApplicationContext [INFO] Closing org.springframework.context.support.GenericApplicationContext@f01a1e: startup date
[Sun Jul 31 17:17:26 BST 2011]; root of context hierarchy 2011-07-31 17:17:27 DefaultLifecycleProcessor [INFO] Stopping beans in phase 2147483647 2011-07-31 17:17:32 DefaultMessageListenerContainer [WARN] Setup of JMS message listener invoker failed for destination 'jemos.tests' -
trying to recover. Cause: Queue with name jemos.tests not found 2011-07-31 17:17:32 DefaultListableBeanFactory [INFO] Destroying singletons in org.springframework.beans.factory.support.
DefaultListableBeanFactory@39478a43: defining beans [helloWorldConsumer,jmsTemplate,org.springframework.jms.listener.DefaultMessageListener
Container#0,destinationManager,configurationManager,jmsConnectionFactory,org.springframework.context.annotation.internalConfigurationAnnotationProcessor
,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.
annotation.internalCommonAnnotationProcessor]; root of factory hierarchy

 

In this test, although we simulated a message roundtrip to a JMS queue, the message never left the current JVM and it the whole execution did not depend on a JMS infrastructure being up. This gives us the power to simulate the JMS infrastructure, to test the integration of our business components without having to fear a red from time to time due to JMS infrastructure being down or inaccessible.

Please note that in the output there are some warnings because the JMS listener container declared in the jemosJms-appContext.xml does not find a queue named «jemos.test» in the fake connection factory, but this is fine; it’s a warning and does not impede the test from running successfully.

The Maven configuration

Here follows the Maven pom.xml to compile the example:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>uk.co.jemos.experiments</groupId>
  <artifactId>jmx-experiments</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>Jemos JMS experiments</name>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.mockrunner</groupId>
      <artifactId>mockrunner</artifactId>
      <version>0.3.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.16</version>     
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.6.1</version>     
      <scope>compile</scope>
    </dependency>   
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.6.1</version>     
      <scope>compile</scope>
    </dependency>   
    <dependency>
      <groupId>org.apache.activemq</groupId>
      <artifactId>activemq-all</artifactId>
      <version>5.5.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.0.5.RELEASE</version>     
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.0.5.RELEASE</version>     
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>3.0.5.RELEASE</version>     
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jms</artifactId>
      <version>3.0.5.RELEASE</version>     
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.0.5.RELEASE</version>
      <scope>test</scope>     
    </dependency>   
   
  </dependencies>
</project> 

From http://tedone.typepad.com/blog/2011/07/mocking-spring-jms-with-mockrunner.html