Test a template method’s implementation

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

Problem

You want to test a template method, but you cannot instantiate the class that defines the method, because the class is abstract.

Background

We generally create template methods from classes in an existing class hierarchy.

We extract them when we notice that each subclass implements a method to per- form the same minitasks (or “primitive operations”) in the same order, even though each subclass might implement some of those primitive operations differ- ently. First, we extract each primitive operation into an appropriately named method [Refactoring, 110]. Next, we pull the larger method up into the super- class [Refactoring, 322], along with any operations that all the subclasses imple- ment the same way. Next, we create abstract methods for the operations that each subclass implements differently, pulling them up into the superclass. The result is a superclass with some abstract methods, declaring abstract operations. We want to test that the template method works, no matter how any subclass decides to implement these abstract operations. We could simply test all the existing sub- classes, but that duplicates testing effort, overestimates our testing progress,16 and misses the point: we want to test all possible subclasses no matter how they imple- ment the abstract operations.

16If you have 10 subclasses, then you will test the template method 10 times, even though each test verifies the same thing. Verifying it 10 times does not make it more correct!

567 Test a template method’s

implementation

Recipe

There are two aspects of a template method to test: behavior and structure. We will describe testing the behavior later in this recipe, but first we will test the struc- ture by verifying that it invokes the expected primitive operations in the expected order. To invoke a template method we need concrete implementations of the abstract operations, which means that we have to create a particular implementa- tion of our abstract class. This appears to contradict our stated goal of testing all possible subclasses, which makes it seem like we cannot do what we have set out to do. In spite of this, there is a straightforward solution.

Consider that a template method consists of invoking a desired set of methods in a desired order. In order to verify this, we need to record the method invoca- tions in order, which we can do with a simple Spy object. The general strategy is to override all the primitive operations to say “I was invoked,” and to collect the order in which the operations report that they were invoked into a List. You can then verify the List of method invocations. This technique is similar to the one Kent Beck calls “Log String.”17 To illustrate the technique, let us verify part of JUnit’s own behavior: the way it runs a test.

Recall that JUnit first invokes setUp(), and then your test method, and then tearDown(). In particular, this is the method that runs your test, from the class junit.framework.TestCase:

public void runBare() throws Throwable { setUp();

try {

runTest();

}

finally { tearDown();

} }

We want to verify that JUnit does indeed invoke these methods in the correct order. We will implement a SpyTestCase which collects the method names run- Bare, setUp, runTest, and tearDown in the order in which they are invoked. Our test will verify the order of those methods. First, here is the test:

public class TestCaseTest extends TestCase {

public void testRunBareTemplate() throws Throwable { SpyTestCase spyTestCase = new SpyTestCase();

spyTestCase.runBare();

17Kent Beck, Test-Driven Development: By Example (Addison-Wesley, 2002), p. 146.

568 CHAPTER 14

Testing design patterns

List expectedMethodNames = new ArrayList() { {

add("setUp");

add("runTest");

add("tearDown");

} };

assertEquals(

expectedMethodNames,

spyTestCase.getInvokedMethodNames());

} }

Next, here is our SpyTestCase, which overrides the methods of interest and records the name of each method as it is invoked:

public class SpyTestCase extends TestCase {

private List invokedMethodNames = new ArrayList();

protected void runTest() throws Throwable { invokedMethodNames.add("runTest");

}

protected void setUp() throws Exception { invokedMethodNames.add("setUp");

}

protected void tearDown() throws Exception { invokedMethodNames.add("tearDown");

}

public List getInvokedMethodNames() { return invokedMethodNames;

} }

We override each primitive operation method and have it just add its name to the list of invoked methods. We also add a method to provide access to the list of invoked method names so that the test can compare that to its expectations. Our SpyTestCase is a “spy” in that it records information about what happened (the order in which the primitive operations were invoked) and provides that “intelli- gence” to the test. This is the essence of the Spy testing pattern, a mock object- related technique. We provide a survey of mock objects in essay B.4, “The mock objects landscape.” You can use this test as a template (yes, we intended the pun) for testing your own template methods.

We started this recipe by describing two types of template method tests: behav- ioral and structural. The Spy technique helps you test the structure of a template method, but you still need to verify that, in general, the template method does

569 Test a template method’s

implementation

what it should. You can use the techniques in recipe 2.6, “Test an interface,” to write tests for the general behavior you expect from your template method.

Returning to our example, we could verify that invoking TestCase.runBare() causes a test to be executed, throwing an AssertionFailedError in the event of a failure.18 To illustrate the difference, here is one of those tests. This test only veri- fies that runBare() reports a failure by throwing an AssertionFailedError, and does not concern itself with the order in which the other methods are invoked, such as setUp() or tearDown().

public void testRunBareExecutesAFailingTest() throws Throwable { TestCase testCase = new TestCase() {

protected void runTest() throws Throwable { fail("Intentional failure");

} };

try {

testCase.runBare();

fail("Test should have failed!");

}

catch (AssertionFailedError expected) {

assertEquals("Intentional failure", expected.getMessage());

} }

We recommend that you separate the behavioral and structural tests, as combin- ing the two can be confusing for particularly complex template methods. As with any of our other recommendations, feel free to try both and measure the results for yourself.

Discussion

It is generally a good idea to test how your template method reacts when one of the primitive operations fails. Because the template method has no control over how a subclass implements a primitive operation, you should assume that the primitive operations can fail. The general strategy for writing this kind of test involves overriding your Spy and having it simulate the desired failure in the appropriate primitive operation. The result is a combination of the Spy technique and the Crash Test Dummy technique. (See essay B.4, “The mock objects land- scape” for more on these mock object techniques.) Returning to JUnit, we want to verify that runBare() invokes tearDown() even when the test fails. To simulate test

18We searched through JUnit to see which methods invoke runBare() and the only one is TestCase.

runProtected(). We used this method to decide how runBare() should behave.

570 CHAPTER 14

Testing design patterns

failure, we can simply override SpyTestCase.runTest() and have it invoke fail()! Listing 14.9 shows the resulting test. We have highlighted in bold print the key dif- ferences between this test and the previous one.

public void testRunBareInvokesTearDownOnTestFailure() throws Throwable {

SpyTestCase spyTestCase = new SpyTestCase() { protected void runTest() throws Throwable { super.runTest();

fail("I failed on purpose");

} };

try {

spyTestCase.runBare();

}

catch (AssertionFailedError expected) {

assertEquals("I failed on purpose", expected.getMessage());

}

List expectedMethodNames = new ArrayList() { {

add("setUp");

add("runTest");

add("tearDown");

} };

assertEquals(

expectedMethodNames,

spyTestCase.getInvokedMethodNames());

}

First, we override SpyTestCase.runTest() and have it do two things: invoke super.runTest() to record that the method was invoked, and then invoke fail() to simulate a generic test failure. Next, we catch the expected AssertionFailed- Error when we invoke runBare(); otherwise, that failure will propagate up the call chain and make it look like our TestCase test failed. When we catch Assertion- FailedError, we check its message so that we can be sure that it is the failure we expect, and not some other failure occurring in the process of executing our test.

Otherwise, our expected result is the same: we expect runBare() to invoke setUp(), runTest(), and then tearDown(). We execute the test and indeed, it passes. If you write a third test and have setUp() throw an exception, you will see

Listing 14.9 TestCaseTest

TE AM FL Y

Team-Fly®

571 Test a template method’s

implementation

that JUnit does not invoke tearDown() in that case. While that piece of informa- tion is not germane to this recipe, it is useful to know. As you can see, testing how a template method handles the failure of one of its primitive operations is just like testing the more general “what happens when this method throws an exception,”

which we described in recipe 2.8, “Test throwing the right exception.”

Finally, we could have made our above test stricter by implementing more of our SpyTestCase. We can actually change the production implementation of run- Bare() by having it invoke another method (say, setName("blah blah blah"), which could be harmful), and our test would be none the wiser. This is a case where we should consider testing not only that the template method does what we expect, but that it also does not do what we do not expect. We could simply over- ride all the methods in TestCase except runBare() to add their name to invoked- MethodNames. This “more nosy” version of SpyTestCase would detect runBare() invoking methods that it should not be invoking. The problem is that whenever we add a method to TestCase, we would have to override it accordingly in SpyTestCase to avoid an “incomplete intelligence report.” This makes our test quite brittle, and we are skeptical of anything that gets in the way of refactoring.

There is an aspect-oriented solution to this problem: intercept every TestCase method invocation and record the name of each method in invokedMethodNames. This solution does not depend on knowledge of the specific methods that TestCase declares, so it is not susceptible to change the way the hand-coded SpyTestCase is. We present a complete solution in solution A.6, “Aspect-based uni- versal Spy.”

Related

■ 2.3—Test a constructor

■ 2.6—Test an interface

■ 2.8—Test throwing the right exception

■ A.6—Aspect-based Universal Spy

572

GSBase

This chapter covers

■ Testing event sources with EventCatcher

■ Testing object serialization and cloning

■ Testing legacy objects for equality

573 GSBase

GSBase (http://gsbase.sourceforge.net) is an open source project maintained by Mike Bowler. Mike is also the lead programmer for HtmlUnit, which we high- lighted in chapter 13, “Testing J2EE Applications.” Mike decided to take his tool- kit of useful Java classes—ones he had developed for his own use and found handy—and create a public project around it, so GSBase contains some general- purpose classes that you might be able to use in your own projects. In this toolkit are a few utilities to make testing with JUnit easier. Mike is a long-time JUnit user and a proponent of Test-Driven Development.

An experienced Java programmer, Mike has been writing Swing applications since, as he puts it, “before Swing 1.0.” As a result, GSBase contains some utilities for Swing programming, among which is EventCatcher, a way to verify Swing UI events. If you write Swing applications—and they are making a comeback as we write these words—then you can use EventCatcher to verify that your event lis- tener receives the events you think it ought to receive. We include a recipe on using EventCatcher in this chapter.

Mike has also given several presentations to Java user groups and at confer- ences on object serialization. In addition to his in-depth knowledge of the topic, he has built a universal serializability test, which helps you verify that you have properly identified your serializable objects as such. We include a recipe for using GSBase’s SerializabilityTest. This is especially important for J2EE applications, where object serialization plays a key role in providing distributed object services.

GSBase also provides a simple test for the clone() method. It is still very com- mon for programmers—in their haste—to forget to make their clone() method public or to forget to add Cloneable to the list of interfaces their class implements.

We include a recipe for using TestUtil.testClone(), which not only tests your implementation, but optionally verifies whether it is consistent with equals().

Finally, GSBase provides a test utility for JavaBeans: “appears equal.” If you have used JavaBeans provided by another programmer or package that do not imple- ment the equals() method, then you can use “appears equal” on those Java beans in your tests. Although it is not a perfect replacement for equals(), it is generally good enough for the vast majority of intended uses, so we include a recipe for using this utility.

There is more to GSBase than we describe here, so we recommend visiting the GSBase site and incorporating this excellent toolkit into your projects. You can also find in table 15.1 the other recipes in this book that use GSBase as part of their solutions.

574 CHAPTER 15 GSBase

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

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

(753 trang)