Test a message-driven bean inside the container

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 445 - 451)

Problem

You want to test a message-driven bean as it executes in production: inside a live EJB container.

Background

The asynchronous nature of message-driven beans makes it difficult for the test to make assertions on the result. A typical test follows the three A’s: arrange, act, assert. Because that typical test has access to all the objects involved, writing asser- tions is not difficult: simply invoke methods on the object and verify their return values. In recipe 2.2, “Test a method that returns nothing,” we discuss how to cope with testing a method that returns no value. The same issues there also apply to testing a message-driven bean, as the message-handling method onMessage() returns no value; but, in the case of a message-driven bean, you do not even invoke the method under test—you send the container a message, and then the container invokes the appropriate message-driven bean. There is no way to obtain a reference to the message-driven bean through a JNDI lookup, so there is no way to observe the side effects onMessage() has. The object under test is in another JVM, executing on another thread, and there is no way for you to obtain a refer- ence to it. This is the severest kind of testing blind.

Recipe

This is perhaps the worst case scenario for Object Testing. We know of no way to write an isolated object test for a message-driven bean running in a live container.

All you can do is send a message to the appropriate destination, wait long enough for the message to be delivered and processed, and then observe whatever exter- nal side effect comes from processing the message. If your message-driven bean updates the database, then you need to test against a live database. If your mes- sage-driven bean sends e-mail, then you need to test against a live e-mail server.

415 Test a message-driven bean

inside the container

This brings with it all the problems inherent in testing with expensive, external resources. We strongly recommend you test message-driven beans outside the container (see recipe 11.8, “Test a message-driven bean outside the container”).

There are some coping mechanisms that you can try, but sometimes the cure is worse than the disease.

You could try substituting a simpler implementation of the external resource in your JNDI directory. As an example, you could extract a MailService interface [Refactoring, 341], and then implement that interface twice: once with a File- System implementation that writes incoming messages to the file system, and once with a JavaMail implementation that sends messages over a real SMTP transport (see figure 11.1). Your message-driven bean looks up the MailService object in your JNDI directory, and your deployment descriptors decide which implementa- tion of MailService it finds: a production deployment includes the JavaMail imple- mentation and a test deployment includes the FileSystem implementation. The details depend entirely on your application server, so we do not present them here.

Now your tests can verify that the message-driven bean has processed a message by waiting for a given file to appear on the file system. The test remains coupled to an expensive, external resource (the file system), but at least you do not need a fully functioning SMTP and POP server. This is a case of introducing a separating interface to hide the implementation details of the service you want to use.23 We ought to mention at this point that if you are happy to do that, then you could save yourself a considerable amount of headache by simply factoring out the message- processing logic from the message-receiving logic and testing each separately, as we suggest in recipe 11.8.

23Robert C. Martin, “The Interface Segregation Principle.” (www.objectmentor.com/publications/

isp.pdf) When an object depends on another, it should depend on the narrowest interface possible.

Message- driven Bean

<<interface>>

MailService

FileSystem Test JNDI

directory

Both JNDI directories

JavaMail implementation

Production JNDI directory implementation

Figure 11.1 Implement MailService differently for tests and for production

416 CHAPTER 11

Testing Enterprise JavaBeans

Assuming you perform these refactorings, here is the kind of test you would write. The test in listing 11.11 submits an order to the appropriate message queue, waits for the ProcessOrderSubmissionBean to process the incoming message and write a reply to the file system, and then the test looks at the file system for the reply and analyzes it.

package junit.cookbook.coffee.model.ejb.test;

import java.io.*;

import javax.jms.*;

import javax.naming.*;

import org.apache.cactus.ServletTestCase;

import com.diasparsoftware.java.io.ReaderUtil;

public class ProcessOrderSubmissionBeanTest extends ServletTestCase { private File tmpDirectory;

private FilenameFilter testMailFilenameFilter;

protected void setUp() throws Exception {

tmpDirectory = new File(System.getProperty("java.io.tmpdir"));

testMailFilenameFilter = new FilenameFilter() { public boolean accept(File dir, String name) { return (name.startsWith("test-mail"));

} };

File[] testMailFiles =

tmpDirectory.listFiles(testMailFilenameFilter);

for (int i = 0; i < testMailFiles.length; i++) { testMailFiles[i].delete();

}

super.setUp();

}

public void testHappyPath() throws Exception { String jbossQueueConnectionFactoryJndiName = "ConnectionFactory";

String orderQueueJndiName = "queue/Orders";

Context context = new InitialContext();

QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory) context.lookup(

jbossQueueConnectionFactoryJndiName);

QueueConnection connection =

queueConnectionFactory.createQueueConnection();

Queue orderQueue =

Listing 11.11 ProcessOrderSubmissionBeanTest

417 Test a message-driven bean

inside the container

(Queue) context.lookup(orderQueueJndiName);

QueueSession session =

connection.createQueueSession(

false,

QueueSession.AUTO_ACKNOWLEDGE);

connection.start();

MapMessage message = session.createMapMessage();

String customerEmailAddress = "jbr@diasparsoftware.com";

message.setString("customer-email", customerEmailAddress);

QueueSender sender = session.createSender(orderQueue);

sender.send(message);

connection.stop();

session.close();

connection.close();

Thread.sleep(500);

File[] testMailFiles =

tmpDirectory.listFiles(testMailFilenameFilter);

assertEquals(

"Too many test files. I don't know which one to look at.", 1,

testMailFiles.length);

File testMailFile = testMailFiles[0];

String testMailText =

ReaderUtil.getContentAsString(new FileReader(testMailFile));

assertTrue(

"Cannot find customer's e-mail address in the reply: "

+ testMailText,

testMailText.indexOf(customerEmailAddress) >= 0);

} }

In the middle of this test we invoke Thread.sleep(500) just to give the message- driven bean enough time to finish its work. We dislike having to pause like this, partly because it introduces an unnecessary delay, but mostly because there is no guarantee that 500 milliseconds is enough time. No amount of “sleep time” is guaranteed to be enough. As a result, this test’s behavior is somewhat random: it is possible for the production code to behave correctly and for this test to fail, because perhaps garbage collection occurs on the application server JVM at exactly the wrong moment. This is another small issue that leads us away from testing message-driven beans in the container. The little things, given enough time, can really add up.

418 CHAPTER 11

Testing Enterprise JavaBeans

Discussion

This test is rather brittle, which is the nature of in-container testing. Here are some of the things that could go wrong with the test:

■ Someone could move the Orders queue to another JNDI name. This would stop the message-driven bean from receiving the message. You would not know that this was the problem until you looked at the application server log and saw that the bean had not done anything.

■ Someone could incorrectly configure the FileSystemMailService. You would not know that this was the problem until you looked at the application server log and perhaps saw nothing! Of course, if you test with a real e-mail address, you will know the production system works when you see the e-mail pile up in that address’ inbox.

■ Someone could change the directory to which the FileSystemMailService writes the e-mails and forget to change the tests. This is one problem whose symptoms are clear, although the cause might not be so obvious. With so many things to go wrong, you might check five different configuration set- tings before thinking of this particular problem.

■ The message-driven bean could slow down momentarily, due either to gar- bage collection or other machine activity. This is the worst kind of problem, because of the potential for false failures. There are two big problems with false failures: first, they waste your time, hunting down nonexistent problems;

and second, the more false failures you handle, the less sensitive you become to test failures, and this second problem is by far more serious than the first. If a failing test becomes nothing to get excited about, then why test at all?

NOTE Be careful testing against “the real thing”—If you want to test code that sends a fax, be careful testing with a real fax machine. Imagine injecting an infinite loop into your production code, sending the same fax over and over again. Now imagine the fax machine is not nearby and no one notices the problem for hours! We found it funny, but some people defi- nitely did not find it funny. (It was not our fax machine.)

You can see now why we emphasize testing outside the container. For message- driven beans in particular, it is very important, because your test is not a direct cli- ent of the object it tests. This means that there is no direct way for the message- driven bean to communicate its behavior back to the test, unless, of course, you consider adding a response message just for testing. We really do not think that is a good idea. If there is absolutely no other reason to return a response message

419 Test a message-driven bean

inside the container

from your message-driven bean, then it makes little sense to add one simply to examine the response. This is one case where the cost outweighs the benefit.

We have suggested in other parts of this book that testing is so important that it is worth adding methods to an interface (as an example) just for testing. We stand by that statement, mostly because creating methods “just for testing” tends to mask a deeper design problem that you might wish or need to solve. We agree that an ideal design can be tested as is, but if we all created ideal designs, there would be no need for this book! The methods we tend to add to these interfaces, though, are “query” methods in the sense of the Command/Query Separation Principle.24 As such, they have no potential side effects to disturb the system.

Sending a response message when the system did not originally require one has the potential to significantly disturb the system.

At a minimum, using this practice for all message-driven beans could double the overall message traffic, which might have strong performance implications. You would also need to configure an extra message queue, and set the JMSReplyTo and CorrelationID properties on your outgoing messages. This is too much to add just to write tests. Because writing tests is important, the only option left is to do some refac- toring without a safety net. This is one case where we believe the rewards outweigh the risk. We would even be tempted to test-drive the message-driven bean from scratch, but that would depend on the specific situation. We would rather test-drive a new class than try to refactor one without tests. See recipe 11.8 for the details.

This book is little more than a snapshot of what we know at this particular time.

Since we first wrote this recipe, we discovered an article that described integrating a Simple SMTP server into a test environment.25 If you like their approach, then you can integrate it into your system by creating a custom JavaMail Provider—assuming your application server supports this feature—and having it ask the Simple SMTP server for a JavaMail Session. As long as both the message-driven bean and the test use the default Session instance, the message-driven bean can send messages to that Session and the test can verify the messages using that Session. As always, we recommend you try out the various approaches and use what works well for you.

Related

■ 11.8—Test a message-driven bean outside the container

■ Command/Query Separation

(http://c2.com/cgi/wiki?CommandQuerySeparation)

24http://c2.com/cgi/wiki?CommandQuerySeparation

25www.javaworld.com/javaworld/jw-08-2003/jw-0829-smtp_p.html.

420 CHAPTER 11

Testing Enterprise JavaBeans

Một phần của tài liệu Manning JUnit recipes practical methods for program (Trang 445 - 451)

Tải bản đầy đủ (PDF)

(753 trang)