◆ Problem
You want to test an event source.
◆ Background
You might be wondering how to test an Observable (or Event Source) without involving the Observer (or Event Listener). After all, how are you supposed to ver- ify that an object generates the right events if you do not listen for those events?
Well, yes, without an event listener there is no way to verify that the event source works; however, just because you need an event listener does not mean you need the production event listener. On the contrary, any event listener will do. The sim- plest kind of event listener does nothing important when it receives an event, except possibly remember the events it received. This recipe shows you how to leverage this observation—pun intended—to test an event source.
◆ Recipe
To test an Observable, you need to recreate the conditions under which you expect an event to be generated, and then verify that it notified its listeners. We often implement this kind of test using the Self-Shunt Pattern. In particular, the test case class implements the event listener interface, collects the events it receives in a List, and then verifies the received events against a List of expected events.
TE AM FL Y
Team-Fly®
551 Test an Observable (Event Source)
To illustrate this, we look once again to JUnit for an example. In recipe 14.1 we tested a text-based implementation of TestListener, which printed information in response to various test-execution events. Now it is time to verify that JUnit gen- erates those events correctly. To generate those events requires executing a test—
the TestResult object we pass into the TestCase when executing the test acts as the Observable. We warn you: testing JUnit with JUnit is a little like trying to chase your own tail, but it is a worthy exercise.6 First, we set up the Self-Shunt: the test case class implements TestListener and collects the various method invocations as events. Because TestResult does not generate event objects, we collect the information about each event handler method invocation in a convenient pack- age: a List. Listing 14.4 shows our test, with the Self-Shunt bits highlighted in bold print.
package junit.cookbook.patterns.test;
import java.util.*;
import junit.framework.*;
public class TestCaseEventsTest extends TestCase
implements TestListener { private List events;
protected void setUp() throws Exception { events = new ArrayList();
}
public void addError(Test test, Throwable throwable) { events.add(
Arrays.asList(
new Object[] { "addError", test, throwable }));
}
public void addFailure(Test test, AssertionFailedError failure) { events.add(
Arrays.asList(
new Object[] { "addFailure", test,
failure.getMessage()}));
}
6 Consider that JUnit was built using JUnit, and done so test-first. See Kent Beck’s Test-Driven Development:
By Example (Addison-Wesley, 2002) to see how to build a Python-based xUnit framework using itself!
Listing 14.4 Verifying the events generated when running a JUnit test
552 CHAPTER 14
Testing design patterns
public void endTest(Test test) {
events.add(Arrays.asList(new Object[] { "endTest", test }));
}
public void startTest(Test test) {
events.add(Arrays.asList(new Object[] { "startTest", test }));
} }
With this in place, we need to test executing a test. (We warned you it would sound weird.) The simplest kind of test to execute is a passing test. We create a passing test, attach our Spy TestListener to a TestResult, and then execute the test using our TestResult. Get it? Maybe not. The code in listing 14.5 makes it more clear.
public class TestCaseEventsTest extends TestCase
implements TestListener {
// code from previous listing omitted public TestCaseEventsTest(String name) { super(name);
}
public void dummyPassingTest() { }
public void testPassingTestCase() throws Exception { final TestCase testCase =
new TestCaseEventsTest("dummyPassingTest");
TestResult testResult = new TestResult();
testResult.addListener(this);
testCase.run(testResult);
List expectedEvents = new ArrayList();
expectedEvents.add(
Arrays.asList(new Object[] { "startTest", testCase }));
expectedEvents.add(
Arrays.asList(new Object[] { "endTest", testCase }));
assertEquals(expectedEvents, events);
}
What better test case class to use to create our dummy passing test than the cur- rent class? (Again, we warned you about how it would sound.) We create a dummy
Listing 14.5 Adding a dummy passing test
553 Test an Observable (Event Source)
passing test and intentionally name it in such a way that the default test suite will not pick it up. Why? When we execute the default TestCaseEventsTest suite, we do not want to include the dummy tests. Instead, we want the TestCaseEventTest tests to execute the dummy tests.7 In testPassingTestCase() we instantiate the test, pass it a TestResult with our TestListener attached, and execute the test case expecting the “start” and “end” events and nothing else. This test passes. So far, so good, but does our test listener handle failing tests? To find out, write a dummy failing test, as in listing 14.6.
public class TestCaseEventsTest extends TestCase
implements TestListener {
// code from previous listings omitted public void dummyFailingTest() { fail("I failed on purpose");
}
public void testFailingTestCase() throws Exception { final TestCase testCase =
new TestCaseEventsTest("dummyFailingTest");
TestResult testResult = new TestResult();
testResult.addListener(this);
testCase.run(testResult);
List expectedEvents = new ArrayList();
expectedEvents.add(
Arrays.asList(new Object[] { "startTest", testCase }));
expectedEvents.add(
Arrays.asList(
new Object[] { "addFailure", testCase,
"I failed on purpose" }));
expectedEvents.add(
Arrays.asList(new Object[] { "endTest", testCase }));
assertEquals(expectedEvents, events);
}
7 By now you must realize that we are just trying to have fun with you. If the words are confusing, then look at the code. Take your time and try to keep it all straight. If you understand this, then you really understand JUnit.
Listing 14.6 Adding a dummy failing test
554 CHAPTER 14
Testing design patterns
We have highlighted in bold print both the failing test and the extra event we expect as a result: a “test failure” event between the “start” and “end” events.
Notice that we compare the failure messages, too—we want TestResult to report the exact failure we expect, or at a minimum, a failure indistinguishable from the one we expect. Either will do. This test also passes. That makes two out of three scenarios—the last occurs when a test throws an exception. This is trickier (as if it has not been tricky enough), so first we present the code in listing 14.7, and then follow it with an explanation.
public class TestCaseEventsTest extends TestCase
implements TestListener {
// code from previous listings omitted private Exception expectedException;
protected void setUp() throws Exception { events = new ArrayList();
expectedException = new Exception("I threw this on purpose");
}
public void dummyExceptionThrowingTest() throws Exception { throw expectedException;
}
public void testError() throws Exception { final TestCaseEventsTest testCase =
new TestCaseEventsTest("dummyExceptionThrowingTest");
TestResult testResult = new TestResult();
testResult.addListener(this);
testCase.run(testResult);
List expectedEvents = new ArrayList();
expectedEvents.add(
Arrays.asList(new Object[] { "startTest", testCase }));
expectedEvents.add(
Arrays.asList(
new Object[] { "addError", testCase,
testCase.expectedException }));
expectedEvents.add(
Arrays.asList(new Object[] { "endTest", testCase }));
assertEquals(expectedEvents, events);
}
Listing 14.7 Adding a test that throws an exception
555 Test an Observable (Event Source)
We have highlighted in bold print the new additions. First the easy part: we expect an “add error” event between the start and end events. So far, so good. To verify that it is the right “add error” event, we need to know which exception (or Throw- able, to be precise) prompted the TestResult to report the error. The exception object we want to verify is the one belonging to the dummyExceptionThrowingTest and not to the testError test, which is why we use testCase.expectedException and not just expectedException. The latter would be an object belonging to test- Error’s fixture, whereas the former belongs to dummyExceptionThrowingTest’s fix- ture. Got it? If not, do not worry—it took us a while to get it too. The point is that we have verified that JUnit generates the four TestListener events correctly by attaching our own TestListener, and then verifying that it receives the events we expected it to receive.
At this point, we can safely conclude that if one listener is correctly notified, then so would any number of registered listeners; however, if you are at all unsure, write one test that registers three event listeners, generates an event, and verifies that all three event listeners were notified. The odds are slim that this would work for one type of generated event and not the others; but if you think there is a greater chance that this might fail, then by all means, write more tests. Our expe- rience has shown that more tests are warranted when testing legacy Observables and fewer are needed when we are building our own. Test until fear turns to boredom.
◆ Discussion
If for some reason you cannot use the Self-Shunt Pattern for this kind of test—and that is rare—we recommend using EasyMock. Each incoming event corresponds to invoking a particular event handler method with certain parameters, and Easy- Mock is optimized for recording and verifying method invocation sequences. Set up a mock event listener with EasyMock, record the event handler method invoca- tions you expect, invoke yourMockControl.replay(), invoke the Observable so that it generates its events, and then invoke yourMockControl.verify(). Once you get the implementation pattern down, it is easy to test more and more events.
That said, the Self-Shunt Pattern is simpler in that it results in less test code, and so we prefer it.
If you find an Observable difficult to test, then the principle cause is violating the Single Responsibility Principle. The most common symptom of this is writing a large amount of code to recreate the situation in which a given event is generated—
or worse, being unable to recreate that situation at all. If you find yourself trying to test such an Observable, then you need to refactor the Observable, extracting the smallest amount of code that will generate the expected event. You can typically
556 CHAPTER 14
Testing design patterns
achieve this by extracting a method [Refactoring, 110]. Your test would then attach itself as an event listener, invoke the newly extracted method, and then verify the event received. The details of the refactoring you need depend entirely on the spe- cifics of your design, so we are unable to give you more specific advice. As usual, test- ing difficulties usually point to objects with too many responsibilities.
◆ Related
■ 14.1—Test an Observer
■ Self-Shunt Pattern
(www.objectmentor.com/resources/articles/SelfShunPtrn.pdf)
■ Robert C. Martin, “The Single Responsibility Principle,”
(www.objectmentor.com/resources/articles/srp)