Mocking JMS infrastructure with MockRunner to favour testing

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

 

This article shows *one* way to mock the JMS infrastructure in a Spring JMS application. This allows us to test our JMS infrastructure without actually having to depend on a physical connection being available. If you are reading this article, chances are that you are also frustrated with failing tests in your continuous integration environment due to a JMS server being (temporarily) unavailable. By mocking the JMS provider, developers are left free to test not only the functionality of their API (unit tests) but also the plumbing of the different components, e.g. in a Spring container.

In this article I show how a Spring JMS Hello World application can be fully tested without the need of a physical JMS connection. I would like to stress the fact that the code in this article is by no means meant for production and that the approach shown is just one of many.

The infrastructure

For this article I use the following infrastructure:

  • Apache ActiveMQ, an open source JMS provider, running on an Ubuntu installation
  • Spring 3
  • Java 6
  • MockRunner
  • Eclipse as development environment, running on Windows 7

The Spring configuration

It's my belief that using what I define as Spring Configuration Strategy Pattern (SCSP) is the right solution in almost all cases when there is the need for a sound testing infrastructure. I will dedicate an entire article to SCSP, for now this is how it looks:

The Spring application context

Here follows the content of 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>

The only important thing to note here is that there are some services which rely on an existing bean named jmsConnectionFactory but that such bean is not defined in this file. This is key to the SCSP and I will illustrate this in one of my future articles.

The Spring application context implementation

Here follows the content of jemosJms-appContextImpl.xml which could be seen as an implementation of the Spring application context defined above

<?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>

This Spring context file imports the Spring application context defined above and it is this application context which declared the connection factory.

This decoupling of the bean requirement (in the super context) from its actual declaration (Spring application context implementation) represents the cornerstore of SCSP.

Mocking the JMS provider - The Spring Test application context and MockRunner

Following the same approach I used above, I can now declare a fake connection factory which does not require a physical connection to a JMS provider. Here follows the content of jemosJmsTest-appContext.xml. Please note that this file should reside in the test resources of your project, i.e. it should never make it to production.

<?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>

Here the Spring test application context file imports the Spring application context (not its implementation) and it declares a fake connection factory, thanks to the MockRunnerMockQueueConnectionFactory class.

A POJO listener

The job of handling the message is delegated to a simple POJO, which happens to be declared also as a 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);

    }

}

There is nothing glamorous about this class. In real life this should have probably be the implementation of an interface, but here I wanted to keep things simple.

A simple JMS message producer

Here follows an example of a JMS message producer, which would use the real JMS infrastructure to send messages:

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");

    }
    
}

The only thing of interest here is that this class retrieves the real JmsTemplate to send a message to the queue.

Now if I was to run this class as is, I would obtain the following:

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

Writing the integration test

There are various interpretations as to what different types of tests mean and I don't pretend to have the only answer; my interpreation is that an integration test is a functional test which also wires up different components together but which does not interact with real external infrastructure (e.g. a Dao integration test fakes data, a JMS integration test fakes the JMS physical connection, an HTTP integration test fakes the remote Web host, etc). Whereas in my opinion, the main purpose of a unit (aka functional) test is to let the API emerge from the tests, the main goal of an integration test is to test that the plumbing amongst components works as expected so as to avoid surprises in a production environment.

Both unit (functional) and integration tests should run very fast (e.g. under 10 minutes) as they constitute what can be considered the "development token". If unit and integration tests are green one should feel pretty confident that 90% of the functionality works as expected; in my projects when both unit and integration tests are green I let developers free to release the token. This does not mean that the other 10% (e.g. the interaction with the real infrastructure) should not be tested, but this can be delegated to system tests which run nightly and don't require the development token. Because unit and integration tests need to run fast, interaction with external infrastructure should be mocked whenever possible.

Here follows an integration test for the Hello World handler:

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] Refreshingorg.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.DefaultMessageListenerContainer#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
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] Closingorg.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.DefaultMessageListenerContainer#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>

猜你喜欢

转载自huanyue.iteye.com/blog/2068309
jms