◆ Problem
You want to test a JMS message producer.
◆ Background
The vast majority of the code you write to send a JMS message is what we some- times call “JMS noise.” There is this large, repetitive structure of code to write before you can send a simple text message such as “Hello.” Especially for novice JMS programmers writing code with an open book next to the keyboard, it is easy to start practicing “copy and paste reuse,” which does nothing except duplicate this JMS noise throughout an application. The most direct way to test a JMS producer is to start a messaging server, connect a listener to the appropriate queue, create a message, send it, and then verify that the listener received it. Although this kind of test is excellent for verifying that you have configured your JMS messaging server correctly, it gets in the way of testing the important part: what you do with the messaging server—the messages you send. After writing one small set of deploy- ment and configuration tests, you ought to focus on the key questions: are we sending the right messages? Are we sending them to the right place? After that, you can trust your application server’s JMS server implementation to work. If not, then we recommend that you do not use it. Either way, don’t test the platform.
◆ Recipe
There are essentially two parts to this recipe. First, we strongly recommend you refactor the JMS noise out to a separate class [Refactoring, 149, 345]. There really ought to be a simpler API for sending simple messages—convenience methods set up for just that purpose. If it were up to us, the JMS API would include a conve- nience API that allows the programmer to send a message with a single method invocation. As there is no such standard API, you either need to find someone who has implemented one or build your own.30 We built the interface MapMessage-
30You may be tempted to build a utility class with class-level methods for sending the various kinds of mes- sages you need. We recommend avoiding class-level methods. See recipe 14.4, “Test a Singleton’s client.”
434 CHAPTER 11
Testing Enterprise JavaBeans
Sender which does just that: it sends a MapMessage to a particular destination—in our case, a Queue. Listing 11.16 shows the result.
package com.diasparsoftware.javax.jms;
import java.util.Map;
public interface MapMessageSender { void sendMapMessage(
String destinationQueueJndiName, Map messageContent)
throws MessagingException;
}
This is a simplifying interface: its job is to hide some of the details—the “JMS noise”—behind simpler method invocations. To send a MapMessage we only need to indicate the JNDI name of the destination Queue and provide a Map containing the message contents. An implementation of this interface does the rest for us.
Although we omit it here for brevity, we implemented MapMessageSender for JBoss to use the JBoss JMS server. By inserting an interface between our message produc- ers and our JMS server, we remove the potential for duplicating JMS client code throughout the application. This is the first step in testing a JMS message producer.
In order to test your message producer without running the messaging server, you need to separate its key responsibilities: creating the message content (not the Mes- sage object, but what it contains), specifying the message’s destination, and using the JMS server. Here is a quick summary of how to test each of these responsibilities.
Creating the message content
We prefer testing message content-generating code separately; the more complex the content, the more important this testing becomes. With a MapMessage, for example, you could extract the ability to add data to a MapMessage from a Map into a separate method (or class), test it once, and use it forever [Refactoring, 110].
You will want to test this behavior in isolation for all but the simplest cases. With MapMessage, for example, it is easy to forget that MapMessage.setObject() only supports the primitive wrapper classes (Integer, Long, and so on) and String, but not arbitrary objects. This is enough to get wrong, so it is enough to test on its own. Listing 11.17 shows an example of such a test, which tries to add an Array- List object to a MapMessage.
Listing 11.16 MapMessageSender
435 Test a JMS message producer
package com.diasparsoftware.javax.jms.test;
import java.util.*;
import javax.jms.*;
import junit.framework.TestCase;
import com.diasparsoftware.javax.jms.*;
import com.sun.jms.MapMessageImpl;
public class BuildMapMessageTest extends TestCase { private MapMessageImpl mapMessage;
private MessageBuilder messageBuilder;
protected void setUp() throws Exception { mapMessage = new MapMessageImpl();
messageBuilder = new MessageBuilder();
}
public void testGenericObject() throws Exception {
Map singleton = Collections.singletonMap("b", new ArrayList());
try {
messageBuilder.buildMapMessage(mapMessage, singleton);
fail("Added a generic object to a MapMessage!");
}
catch (MessagingException expected) {
Throwable throwable = expected.getCause();
assertTrue(
"Wrong exception type",
throwable instanceof MessageFormatException);
} } }
You will find similar tests useful, depending on what types of messages you use in your system. When building an ObjectMessage, be sure the Object is Serializ- able. When building a StreamMessage, be sure you are streaming the contents in the expected order. These are the kinds of tests you ought to write for the differ- ent kinds of messages you build. The test we have written here helps us be sure that we can build a MapMessage correctly. All that is left is to verify that each mes- sage producer passes in the correct content (Map object) depending on the con- tent of the message they want to send. Listing 11.18 shows the “happy path” test.
package junit.cookbook.coffee.model.logic.test;
import java.util.*;
import junit.cookbook.coffee.model.*;
Listing 11.17 BuildMapMessageTest
Listing 11.18 The “happy path” test for submitting an order
436 CHAPTER 11
Testing Enterprise JavaBeans
import junit.cookbook.coffee.model.logic.SubmitOrderCommand;
import junit.framework.TestCase;
import org.easymock.MockControl;
import com.diasparsoftware.javax.jms.MapMessageSender;
public class SubmitOrderTest extends TestCase { private MockControl mapMessageSenderControl;
private MapMessageSender mapMessageSender;
private Customer jbrains;
private Order order;
protected void setUp() throws Exception { mapMessageSenderControl =
MockControl.createControl(MapMessageSender.class);
mapMessageSender =
(MapMessageSender) mapMessageSenderControl.getMock();
jbrains = new Customer("jbrains");
jbrains.emailAddress = "jbr@diasparsoftware.com";
Set orderItems =
Collections.singleton(
new CoffeeQuantity(3, "Special Blend"));
order = new Order(new Integer(762), jbrains, orderItems);
}
public void testHappyPath() throws Exception { Map expectedMessageContent =
Collections.singletonMap(
"customer-email", jbrains.emailAddress);
mapMessageSender.sendMapMessage(
"queue/Orders",
expectedMessageContent);
mapMessageSenderControl.setVoidCallable();
mapMessageSenderControl.replay();
SubmitOrderCommand command = new SubmitOrderCommand();
command.setOrder(order);
command.execute(mapMessageSender);
mapMessageSenderControl.verify();
} }
Here we use EasyMock to mock the MapMessage sender, because we have already tested it separately. We verify the message content (the Map object) by examining the parameter that the SubmitOrderCommand passes to the MapMessageSender.
437 Test a JMS message producer
Of course, we should add other tests to cover various error cases, such as an invalid Order object.
Verifying the message destination
The previous test killed two birds with one stone, as it were: in addition to verify- ing the message content parameter, we used EasyMock to verify the destination queue for the message. Once again, we have already tested whether MapMessage- Sender uses that destination parameter when sending a message, so all the mes- sage producer needs to do is to specify the destination correctly. This is one area where error-case testing is important: JMS implementations have a large number of moving parts, so you want to be sure that you handle JMS exceptions properly.
In our design, the implementations of MapMessageSender wrap JMS exceptions in a more general MessagingException. The latter is an unchecked exception, which reduces unnecessary coupling between message clients and our JMS-integration objects. We may, for example, want to verify that if the destination we specify does not exist, then the SubmitOrderCommand reports the exception in a useful way.
Here is such a test. We use EasyMock to simulate MapMessageSender throwing a MessagingException:
public void testQueueDoesNotExist() throws Exception { Map expectedMessageContent =
Collections.singletonMap(
"customer-email", jbrains.emailAddress);
mapMessageSender.sendMapMessage(
"queue/Orders",
expectedMessageContent);
MessagingException destinationNotExistException = new MessagingException(
"Unable to send message",
new JMSException("Destination does not exist"));
mapMessageSenderControl.setThrowable(
destinationNotExistException);
mapMessageSenderControl.replay();
try {
SubmitOrderCommand command = new SubmitOrderCommand();
command.setOrder(order);
command.execute(mapMessageSender);
fail("Did not throw exception?");
}
catch (CommandException expected) { assertEquals(
438 CHAPTER 11
Testing Enterprise JavaBeans
"Unable to submit order " + order, expected.getMessage());
assertSame(
destinationNotExistException, expected.getCause());
}
mapMessageSenderControl.verify();
}
Here we verify that the SubmitOrderCommand reports the problem from the domain’s perspective: “unable to submit order.” If it were merely to report “Queue does not exist: queue/Orders,” then it might or might not be clear to the person reading the message where the problem lies. Yes, the stack trace would help, but in a production environment there might not be any line numbers to help, even if one could retrieve that particular revision of the source code!31 The more the errors communicate, the better. This test helps improve the way in which the sys- tem communicates this kind of problem.
Using the messaging server
Now we need to verify that the JMS-integration code works as we would expect. In our case, this is an implementation of MapMessageSender that actually sends the message using the JMS API. We recommend testing this behavior with a live con- tainer, as only then can you be certain that the results are meaningful. The tests can be simple, they have nothing to do with your problem domain and you can use them almost exclusively to help isolate defects reported from outside the pro- gramming team. Best of all, the MapMessageSender is something you can use across projects, and so if you test it once, you can treat it as a trusted component on future projects. If you can get the same quality with fewer tests, then so much the better. The technique is straightforward: start the JMS server, register a Message- Listener, send the message, and verify that it was received. There are few enough of these tests that you can accept the cost of testing against an expensive, external resource. This is another case in which we choose simpler tests over faster tests.
◆ Discussion
If you are adamant about testing the message-sending behavior without a messag- ing server, then you can use EasyMock and MockEJB’s JNDI implementation (MockContext) to verify that your message-sending code invokes the appropriate
31Many organizations we have worked with have not been disciplined in their configuration management.
How quickly could you get the source for the third-to-last release of your product?
439 Test the content of your JNDI directory
API methods. Just be aware that this requires five mock objects: the queue connec- tion factory, queue connection, queue session, queue sender, and the queue itself. Not only that, the test virtually duplicates the underlying code, so it proves little and is easy to get wrong. Using the JMS API correctly is the point of the entire test: invoking the correct methods in the correct order to make the JMS server send your message. The process is complex enough that we recommend against this kind of test. When an external resource requires that much code to simulate its behavior, take it as a sign that too much can go wrong to make it worth writing a simulator. The more you need to mock, the more risk you run in getting the test wrong—after all, how would you know? If you set up your mock JMS objects incor- rectly, how would you discover the problem? Probably not until you tried to run your JMS-integration code against a live messaging server. If that is the case, then test against the live server, but keep the integration as small as possible. This is the approach we take in this recipe and a general approach we have recommended elsewhere in this book. See chapter 10, “Testing and JDBC,” for details on mini- mizing the size of a JDBC integration layer.
By the way, although we have focused the discussion on map messages and queues, the underlying principles apply just as well to the other message types and topics. We would not want to make you feel as though they need to be handled any differently.
◆ Related
■ Chapter 10—Testing and JDBC
■ 14.4—Test a Singleton’s client