Test an object that instantiates other objects

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

Problem

You want to test an object in isolation, but it instantiates other objects that make testing difficult or expensive.

Background

There are opposing forces in object-oriented design. We use aggregation to express the notion that an object owns another object to which it refers, much the way a car owns its wheels. On the other hand, to test an object in isolation, we need to be able to piece objects together like a jigsaw puzzle. This means that tests prefer objects that use composition more than aggregation. If the object you want to test instantiates other objects, then it is difficult to test the larger object without rely- ing on the correctness of the smaller object, and that violates the principle of test- ing objects in isolation.

We find it unfortunate that a majority of programmers remain in the dark about the power of extensive Programmer Testing. One side effect of this trend is an overuse of aggregation. Designs are replete with objects that instantiate other objects or retrieve objects from globally accessible locations. These programming practices, when left unchecked, lead to highly coupled designs that are difficult to test. We know: we have inherited them and even built some of them ourselves.

This recipe introduces one of the fundamental testing techniques leading to a small design improvement, making it possible to test an object in isolation.

Recipe

To deal with this problem, you need to pass the object under test an alternate implementation of the object it plans to instantiate. This creates two small prob- lems that you need to solve:

67 Test an object that instantiates other objects

■ How do you create an alternate implementation of this object’s collaborator?

■ How do you pass it in to the object?

To simplify the discussion, we will use the term Test Object (not to be confused with Object Test—sorry about that) to refer to an alternate implementation of a class or interface that you use for testing. There are a number of different kinds of Test Objects: fakes, stubs, and mocks, and we discuss the differences in the essay “The mock objects landscape” in appendix B.

Creating a Test Object out of an interface is simple: just create a new class and implement the interface the simplest way you can, and you have finished. This is the simplest kind of Test Object. We use EasyMock (www.easymock.org/) through- out this book to generate Test Objects for interfaces.16 We use this package both because it eliminates some repetitive work and because it adds some consistency and uniformity to the way we fake out interfaces. This makes our tests easier to understand—at least to programmers familiar with EasyMock! You can find numerous examples of using EasyMock to create Test Objects out of the J2EE interfaces in part 2.

Creating a Test Object out of a class can be as simple as creating a subclass and then either faking out or stubbing out all its methods.17 A fake method returns some predictable, meaningful, hard-coded value, whereas a stub method does nothing meaningful—only what is required to compile. You may find it advanta- geous to extract an interface [Refactoring, 341] and change the class under test so that it refers to its collaborator through an interface, rather than the class. This allows you to use EasyMock as we described previously. Beyond that, the more your classes collaborate with one another through interfaces, the more flexible (and testable!) your design.

As for the second part of the problem, there are essentially two ways to pass your Test Object into the object under test: either augment the constructor or add a set- ter method. We recommend passing Test Objects into the constructor simply to avoid the extra complexity of having to invoke the setter method in the test. To illustrate this technique, take the example from J. B.’s article “Use Your Singletons Wisely.”18 In it, a Deployment uses a Deployer to deploy something to a file. In the

16Had we written this chapter a few months later, we would have been using jMock (http://jmock.code- haus.org/), but that is the risk you take when writing a book.

17Again, jMock makes this easier using CGLib (http://cglib.sourceforge.net/). Through bytecode manipulation, you can fake concrete classes as easily as interfaces.

18www-106.ibm.com/developerworks/webservices/library/co-single.html.

68 CHAPTER 2 Elementary tests

example, there only needs to be one Deployer, so it is easily designed as a Single- ton. (See recipe 14.3, “Test a Singleton,” for more on testing and Singletons.) The method Deployment.deploy(), then, looks as follows:

public class Deployment {

public void deploy(File targetFile) throws FileNotFoundException { Deployer.getInstance().deploy(this, targetFile);

} }

Notice that Deployment uses the class-level method Deployer.getInstance() to obtain its Deployer. If you want to fake out the Deployer, you need to pass a Deployer into Deployment somehow. We recommend passing it in through a con- structor, so we add a constructor and instance variable to store the Deployer:

public class Deployment { private Deployer deployer;

public Deployment(Deployer deployer) { this.deployer = deployer;

}

public void deploy(File targetFile) throws FileNotFoundException { deployer.deploy(this, targetFile);

} }

But wait! Where did Deployer.getInstance() go? We cannot just lose this bit of code: now that we have removed the no-argument constructor, we need to add a new one and have it supply the Singleton Deployer by default:

public class Deployment { private Deployer deployer;

public Deployment() {

this(Deployer.getInstance());

}

public Deployment(Deployer deployer) { this.deployer = deployer;

}

public void deploy(File targetFile) { deployer.deploy(this, targetFile);

} }

Now when the production code creates a Deployment using the no-argument con- structor, it will see the behavior it has come to expect: the Deployment will use the Singleton Deployer. Our tests, however, can substitute a fake Deployer in order to

69 Test an object that instantiates other objects

do things such as simulate what happens if the target deployment file does not exist. Here is the code for a “crash test dummy” Deployer—one that always signals that the target file does not exist:

public class FileNotFoundDeployer extends Deployer {

public void deploy(Deployment deployment, File targetFile) throws FileNotFoundException {

throw new FileNotFoundException(targetFile.getPath());

} }

Now we can test how our Deployment class behaves when the Deployer fails to deploy because the target file was not found. We use the technique in recipe 2.8,

“Test throwing the right exception”:

public void testTargetFileNotFound() throws Exception {

Deployer fileNotFoundDeployer = new FileNotFoundDeployer();

Deployment deployment = new Deployment(fileNotFoundDeployer);

try {

deployment.deploy(new File("hello"));

fail("Found target file?!");

}

catch (FileNotFoundException expected) {

assertEquals("hello", expected.getMessage());

} }

This test shows how to substitute a Test Object in place of an object’s collaborator, as well as how to create a Test Object by subclassing the production class. We effectively make the collaborator an optional parameter to the constructor: if we do not pro- vide one, then the class provides a sensible default implementation. We use this tech- nique and variations of it throughout the book and indeed throughout our work.

Discussion

We have used a Test Object to simulate not being able to find the target file, rather than re-creating that scenario, which would involve trying to deploy to a real nonexistent file on the file system. We strongly recommend simulating these con- ditions, because re-creating them can be prone to error. What happens if you specify a Windows filename for your nonexistent file, and then someone executes the test on a UNIX machine? On the UNIX file system, your Windows filename may not even be a valid filename. Worse, what if you happen to choose a “nonex- istent” filename that matches a file residing on someone’s machine? You could use JVM properties to look for the machine’s temporary directory, but all in all it is simpler to simulate the error condition than re-create it.

70 CHAPTER 2 Elementary tests

Faking out class-level and global data and methods is difficult, because you can- not override a class-level method by subclassing; and even if you could, code that uses a class-level method hard-codes the name of the class, defeating your attempt to substitute behavior through subclassing. There is considerable discussion in the JUnit and Test-Driven Development communities regarding the use of class- level methods and how to overcome their use in creating testable designs. The consensus is that it is best to minimize the use of class-level methods by moving them into new classes and making them instance level, at least when it makes sense. There are several coping strategies, including hiding class-level methods behind a facade that makes these methods look like they are instance level. These all require considerable and mechanical refactoring, generally done without the safety net of tests! Fortunately, Chad Woolley has begun building a toolkit that uses aspects to make it possible to fake out even class-level methods.19

His toolkit, named Virtual Mock (www.virtualmock.org/), promises to provide an easier way to build the refactoring safety net that you need to repair highly cou- pled designs—particularly those that make heavy use of class-level methods and data. Although still in the alpha stage as we write these words, Chad’s work is exciting, and we recommend that you add Virtual Mock to your arsenal for the next time you inherit such a design. That said, we strongly recommend using Vir- tual Mock objects to install a safety net for refactoring; do not use it to cover up the bad smells in your design, but rather let it enable you to start cleaning it up.20

Related

■ 2.8—Test throwing the right exception

■ 14.3—Test a Singleton

■ B.4—The mock objects landscape

■ The Virtual Mock project (www.virtualmock.org)

19www.parc.xerox.com/aop.

20Martin Fowler popularized the use of the term smell for something about a program that we don’t like.

We now commonly say that code smells, or design smells, or the process smells, to indicate that some- thing is not quite right.

TE AM FL Y

Team-Fly®

71

Organizing and building JUnit tests

This chapter covers

■ Deciding where to place production code and test code

■ Dealing with duplicate test code

■ Organizing special case tests

■ Building tests in various development environments

72 CHAPTER 3

Organizing and building JUnit tests

Once you understand the fundamentals of writing JUnit tests, the next step is to begin writing them. Once you open your favorite Java editor and begin writing test code, you need to decide into which package you should place the test code.

Although this decision seems simple enough, there is a considerable difference between placing your test code in the same package as your production code1 or in a different package. For that reason, we offer a few recipes to help you decide which to do.

After deciding on a package for your tests, you can only type two words (public class) before you come to the class name of your new test. At this point you need to decide how to organize your tests into test case classes. Many tutorials on JUnit suggest writing one TestCase class per production class; however, those authors generally mean that to be a simple guideline for programmers just starting out. As you write your tests, the names you choose can tell you when it may be time to move tests into a new fixture. We provide a few recipes that describe when to reor- ganize your test code.

In order to execute your new test, you need to save your new source file. You need to decide whether to separate your test source code from your production source code. Some programming environments support multiple source trees quite easily, others do not. We offer some recipes that suggest how to keep from mixing up your tests and your production code.

Before you can execute your test, you need to build the new test class, which brings into question how to organize your build tree. You may need or want to keep your tests entirely separated from your production code, or you may decide to distribute your tests. To help you decide, we offer some recipes that describe each, including when each choice might be particularly desirable.

A place to start

Before diving into the recipes in this chapter, let us review the simplest and most straightforward way to organize your tests. When you are ready to start writing tests, begin by creating a new test case class that corresponds to the class you plan to test. This guideline applies both to existing production classes and to produc- tion classes you are “test driving.” Following our previous advice for naming test case classes (see chapter 1, “Fundamentals”), add Test to the end of the produc- tion class name to get your test case class name. Continuing with our example,

1 We will use the terms production code and test code to differentiate the code that implements your system from the code that tests your system.

73 Organizing and building JUnit tests

you place your first tests for the Money class in a new test case class named Money- Test. To simplify things further, place this test alongside class Money, in the same package and in the same source code directory.

The simplest way to start is to write each test for Money as a method in Money- Test. If your first test verifies that the default constructor sets the underlying Money value to $0.00, then you should have something that resembles listing 3.1.

package junit.cookbook.organizing;

import junit.framework.TestCase;

public class MoneyTest extends TestCase { public void testDefaultAmount() { Money defaultMoney = new Money();

assertEquals(0, defaultMoney.inCents());

} }

As you think of more tests for Money, write each one as a new method in Money- Test. Remember to name your methods starting with “test” so that the JUnit test runners automatically execute them. Eventually you will find that a number of tests use the same objects—that is, objects initialized to the same state. When you see this begin to happen, you may end up with duplicated code within your tests.

We recommend removing that duplication by creating a test fixture (see recipe 3.4,

“Factor out a test fixture”). As you build up fixtures inside your TestCase class, you may find that certain tests use one part of the fixture and other tests use another part of the fixture. This may signal the need to separate your tests into different TestCase classes. Over time, the new test fixtures begin to arrange themselves—as if by magic—into a TestCase hierarchy. See recipe 3.5, “Factor out a test fixture hierarchy,” for details on how to factor out the common parts of existing test fix- tures into a kind of “superfixture.” At some point, you may decide that you need to separate the test source code from the production source code, either to sim- plify distributing your application or even just to keep the tests from “muddling up” your production code. See recipe 3.2, “Create a separate source tree for test code,” for details.

The recipes in this chapter help you organize and build your tests effectively, using best practices acquired over the years through hard experience. We cannot cover all possible—or even feasible—approaches, but we have shared what has worked well for us.

Listing 3.1 Simple Money test

74 CHAPTER 3

Organizing and building JUnit tests

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

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

(753 trang)