Tải bản đầy đủ
1 What is testing, and why do it?

1 What is testing, and why do it?

Tải bản đầy đủ

To enable refactoring
Refactoring is now a well-known concept: the practice of rearranging your code to
keep it clean and to help it adapt to change. As defined by Martin Fowler, refactoring
is “the restructuring of software by applying a series of internal changes that do not
affect its observable behavior” (Fowler 1999). That is, it changes the internals of a
program without changing what it does.
If you’re going to refactor your program, large portions of your source can change
as they’re moved around, restructured, or otherwise refactored—often at the click of
a button in the IDE. After you make those changes, how can you be sure that everything is still working as before? The only way to know is through those tests that you
wrote before you began refactoring, the tests that used to work.
Automated testing transforms how you develop programs. Instead of writing code
and hoping that it works, or playing with a program by hand to reassure yourself that
all is well, testing can show how much of a program really is working. Ant goes hand
in hand with this concept, because it integrates testing with the build process. If it’s
easy to write tests and easy to run them, there’s no longer any reason to avoid testing.

4.2

INTRODUCING OUR APPLICATION
This is the first place in our book where we delve into the application that we built to
accompany this text. We’re going to use this application through most of the remaining chapters.
Why is the “testing” chapter the right place to introduce our application?
Because the tests were written alongside our application: the application didn’t exist
until this chapter.

4.2.1

The application: a diary
We’re going to write a diary application that will store appointments somewhere and
print them out. Later on, the diary will save data to a database and generate RSS feeds
and HTML pages from that database. We’ll add these features as we go along, extending the application, the tests, and the build file in the process.
Using an agile process doesn’t mean we can skip the design phase. We just
avoid overdesigning before implementing anything we don’t immediately need.
Accordingly, the first step for the application is to sketch out the architecture in a
UML tool. Figure 4.1 shows the UML design of the library.
The core of our application will be the Events class, which will store Event
instances. Every Event must have a non-null id, a date, and a name; extra text
is optional. The operation Event.equals() compares only the ID values; the
hashCode() value is also derived from that. The Event.compareTo operator is
required to have the same semantics as the equals operator, so it too works only on
the ID value. To sort events by time, we must have a special Comparator implementation, the DateOrder class. We mark our Event as Serializable to use

INTRODUCING OUR APPLICATION

81

Figure 4.1 UML diagram of the core of our diary. Interfaces and classes in grey are those of the
Java libraries. We’re going to assume they work and not test them ourselves.

Java’s serialization mechanism for simple persistence and data exchange. Oh, and the
ID class itself is the java.util.UUID GUID class.
We aggregate events in our Events class, which provides the manipulation
options that we currently want. It’s also Serializable, as is the class to which it
delegates most of the work, a java.util.HashMap. Provided all elements in the
collection are serializable, all the serialization logic is handled for us. Two methods,
Events.load() and Events.save() aid serialization by saving to/from a file.
We don’t override the equals()/hashCode() logic in our Events class; it’s too
much effort. In keeping with XP philosophy, we avoid writing features until they’re
needed; if they’re not needed, we omit them. This is also why the class exports very
few of the operations supported by its internal map; initially it exports only size()
and iterator().
The Java package for the application is d1 for “diary-1”; the core library that will
go into the package is d1.core.
There. That’s a sketch of the initial design of our program. Is it time to start coding? Almost. We just have to think about how to test the program before we write a
line of code, as that’s the core philosophy of test-first development.

82

CHAPTER 4

TESTING WITH JUNIT

4.3

HOW TO TEST A PROGRAM
Test-first development means writing the tests before the code, wherever possible. Why?
Because it makes developers think about testing from the outset, and so they write programs that can be tested. If you put off testing until afterwards, you’ll neglect it. When
someone does eventually sit down to write a test or two, they’ll discover that the application and its classes may be written in such a way that its almost impossible to test.
The classic way to show that a program works is to write special main methods
on different classes, methods that create an instance of the class and check that it
works as expected. For example, we could define a main method to create and print
some files:
public class Main {
public static void main(String args[]) throws Exception {
Events events = new Events();
events.add(new Event(UUID.randomUUID(),
new Date(), "now", null));
events.add(new Event(UUID.randomUUID(),
new Date(System.currentTimeMillis() + 5 * 60000),
"Future", "Five minutes ahead"));
System.out.println(events);
}
}

We can run this program from the command-line:
java -cp build\classes;build\test\classes d1.core.test.Main

It will print something we can look at to validate:
Wed Feb 16 16:15:37 GMT 2005:Future - Five minutes ahead
Wed Feb 16 16:10:37 GMT 2005:now -

This technique is simple and works with IDEs. But it doesn’t scale. You don’t just
want to test an individual class once. You want to test all your classes, every time
you make a change, and then have the output of the tests analyzed and presented in
a summary form. Manually trying to validate output is a waste of time. You should
also want to have your build stop when the tests fail, making it impossible to ship
broken programs.
Ant can do this, with help. The assistant is JUnit, a test framework that should
become more important to your project than Ant itself. The two tools have a longstanding relationship: JUnit made automated testing easy, while Ant made running
those tests part of every build.
Before exploring JUnit, we need to define some terms.

HOW TO TEST A PROGRAM

83

• Unit tests test a piece of a program, such as a class, a module, or a single method.
They can identify problems in a small part of the application, and often you can
run them without deploying the application.
• System tests verify that a system as a whole works. A server-side application
would be deployed first; the tests would be run against that deployed system,
and may simulate client behavior. Another term for this is functional testing.
• Acceptance tests verify that the entire system/application meets the customers’
acceptance criteria. Performance, memory consumption, and other criteria may
be included above the simple “does it work” assessment. These are also sometimes called functional tests, just to cause extra confusion.
• Regression testing means testing a program to see that a change has not broken
anything that used to work.
JUnit is a unit-test framework; you write tests in Java to verify that Java components
work as expected. It can be used for regression testing, by rerunning a large test suite
after every change. It can also be used for some system and acceptance testing, with
the help of extra libraries and tools.

4.4

INTRODUCING JUNIT
JUnit is one of the most profound tools to arrive on the Java scene. It single-handedly
took testing mainstream and is the framework that most Java projects use to implement their test suites. If you consider yourself a Java developer and you don’t yet know
JUnit, now is the time to learn. We’ll introduce it briefly. (If you wish to explore JUnit
in much more detail, we recommend JUnit in Action by Vincent Massol.) Ant integrates JUnit into a build, so that you don’t neglect to run the tests and to give you nice
HTML reports showing which tests failed.
JUnit is a member of the xUnit testing framework family and is now the de facto
standard testing framework for Java development. JUnit, originally created by Kent
Beck and Erich Gamma, is an API that makes it easy to write Java test cases, test cases
whose execution can be fully automated.
JUnit is just a download away at http://www.junit.org. All JUnit versions can be
downloaded from http://prdownloads.sourceforge.net/junit/. The archive contains
junit.jar—which is the JAR file you need—the JavaDocs, and the source (in src.jar).
Keep the source handy for debugging your tests. We’re using JUnit 3.8.2, not the version 4.x branch.

Why use JUnit 3.8.2 and not JUnit 4.0?
This book uses JUnit 3.8.2 throughout. JUnit 4.0, released in February 2006, is the
successor to this version, as are versions 4.1 and beyond. So why aren’t we using the
4.x branch? Primarily, because the new version isn’t very backwards-compatible with
the existing JUnit ecosystem. JUnit 3.x has been stable for so long that many tools
84

CHAPTER 4

TESTING WITH JUNIT

have built up around it—including Ant itself. Because Ant is designed to work on all
versions of Java from 1.2 upwards, Ant and its own test suite haven’t migrated to the
JUnit 4.x branch.
Ant’s task does work with JUnit 4, so you can run JUnit 4 tests under
Ant. However the generated reports aren’t perfect, as Ant is still running and reporting the tests as if they were JUnit 3.x tests. A new task for JUnit 4 is needed, one that
will probably be hosted in the JUnit codebase itself.
JUnit’s architecture
Figure 4.2 shows the UML model of the JUnit 3.8.2 library. The abstract TestCase
class is of most interest to us.
The TestCase class represents a test to run. The Assert class provides a set
of assertions that methods in a test case can make, assertions that verify that the
program is doing what we expect. Test case classes are what developers write to test
their applications.

Figure 4.2 JUnit UML diagram depicting the composite pattern
utilized by TestCase and TestSuite. A TestSuite contains
a collection of tests, which could be either more TestSuites or
TestCases, or even classes simply implementing the test interface.
The Assert class provides a set of static assertions you can make about
your program.

INTRODUCING JUNIT

85

4.4.1

Writing a test case
The first thing we must do with JUnit is write a test case, a class that contains test
methods. This is easy to do. For a simple test case, we follow three steps:
• Create a subclass of junit.framework.TestCase.
• Provide a constructor, accepting a single String name parameter, which calls
super(name).
• Write some public no-argument void methods prefixed by the word test.
Here is one such test case, the first one for our application:
package d1.core.test;
import junit.framework.TestCase;
import d1.core.Event;
public class SimpleTest extends TestCase {
public SimpleTest(String s) {
super(s);
}
public void testCreation() {
Event event=new Event();
}

Extend the
TestCase

String constructor
that invokes the parent
String constructor
This is the
test method

}

This test actually performs useful work. We have a single test, testCreation, in
which we try to create an event. Until that class is written, the test case won’t compile.
If the Event constructor throws a RuntimeException, the test won’t work. Merely
by trying to instantiate an object inside a test case, we’re testing parts of the application.
With the test case written, it’s time to run it.
4.4.2

Running a test case
Test cases are run by way of JUnit’s TestRunner classes. JUnit ships with two builtin test runners—a text-based one, junit.textui.TestRunner, and a GUI one,
junit.swingui.TestRunner. From the Windows command-line, we could
run the text TestRunner like this:
java -cp build\classes;build\test\classes;
%ANT_HOME%\lib\junit-3.8.2.jar junit.textui.TestRunner
d1.core.test.SimpleTest
.
Time: 0.01
OK (1 test)

The ‘.’ character indicates a test case is running; in this example only one exists,
testCreation. The Swing TestRunner displays success as green and failure as red
86

CHAPTER 4

TESTING WITH JUNIT

Figure 4.3
JUnit’s Swing GUI
has successfully run
our test case. A green
bar indicates that all
is well. If there was a
red bar, we would
have a problem.

and has a feature to reload classes dynamically so that it can pick up the latest test case
classes whenever you rebuild. For this same test case, its display appears in figure 4.3.
Ant uses its own TestRunner, which runs the tests during the build, so the GUI
isn’t needed. Java IDEs come with integrated JUnit test runners. These are good for
debugging failing test cases. Ant can do something the GUIs can’t do: bulk-test hundreds of tests and generate HTML reports from the results. That is our goal: to build
and test our program in one go.
4.4.3

Asserting desired results
A test method within a JUnit test case succeeds if it completes without throwing an
exception. A test fails if it throws a junit.framework.AssertionFailedError or derivative class. A test terminates with an error if the method throws any
other kind of exception. Anything other than success means that something went
wrong, but failures and errors are reported differently.
AssertionFailedError exceptions are thrown whenever a JUnit framework
assertion or test fails. These aren’t Java assert statements, but inherited methods
that you place in tests. Most of the assertion methods compare an actual value with
an expected one, or examine other simple states of Object references. There are variants of the assert methods for the primitive datatypes and the Object class itself.
Diagnosing why a test failed is easier if you provide meaningful messages with
your tests. Which would you prefer to deal with, an assertion that “expected all
records to be deleted,” or “AssertionFailedError on line 423”? String messages
become particularly useful when you have complex tests with many assertions, especially those created with assertTrue(), assertFalse, and fail(), for which
there is no automatically generated text. Table 4.1 lists JUnit’s built-in assertions.

INTRODUCING JUNIT

87

Table 4.1

Assertions that you can make in a JUnit test case

Assertion

Explanation

assertTrue([String message],
boolean condition)

Asserts that a condition is true.

assertFalse([String message],
boolean condition)

Asserts that a condition is false.

assertNull([String message],
Object object)
assertNotNull([String message],
Object object)

Asserts that an object reference is null. The
complementary operation asserts that a reference is
not null.

assertEquals([String message],
Type expected,
Type actual)

Asserts that two primitive types are equal. There are
overloaded versions of this method for all Java’s
primitive types except floating point numbers.

assertEquals([String message],
Object expected,
Object actual)

States that the test expected.equals(actual)
returns true, or both objects are null.

assertEquals([String message],
FloatType expected,
FloatType actual,
FloatType delta)

Equality assertion for floating point values. There are
versions for float and double. The values are
deemed equal if the difference is less than the delta
supplied. If an infinite value is expected, the delta
is ignored.

assertSame([String message],
Object expected,
Object actual)

Asserts that the two objects are the same. This is a
stricter condition than simple equality, as it
compares the object identities using expected
== actual.

assertNotSame([String message],
Object expected,
Object actual

Asserts that the two objects have different
identities, that is, expected != actual.

fail()
fail(String message)

Unconditional failure, used to block off a branch of
the test.

Every test case should use these assertions liberally, checking every aspect of the
application.
Using the assertions
To use the assertions, we have to write a test method that creates an event with sample
data, then validates the event:
public class LessSimpleTest extends TestCase {
public LessSimpleTest(String s) {
super(s);
}
public void testAssignment() {
final Date date = new Date();

88

CHAPTER 4

TESTING WITH JUNIT

Event event = new Event(UUID.randomUUID(),
date, "now", "Text");
assertEquals("self equality failed",
Test that Event.equals()
works for comparing
event,
an object to itself
event);
assertEquals("date not retained",
Test that the date we supplied
date,
in the constructor was used
event.getDate());
String eventinfo = event.toString();
assertTrue("Event.name in toString() "
Evaluate event.toString()
and verify that the name
+ eventinfo,
of the event is returned
eventinfo.contains("now"));
}
}

It’s important to keep test methods as simple as you can, with many separate test methods. This makes analysis easier: there should be only one reason for any test to fail.
For thorough testing, you need lots and lots of tests. In a well-tested project, the
amount of test code may well be bigger than the main source itself. It’s certainly not
trivial to write good, thorough tests. Everyone on the team needs to think it’s important—all the developers, all the management. If anyone thinks that writing tests is a
waste of time, you won’t get the support or coverage you need. You’ll need to convince them otherwise by showing how testing gets applications working faster than
shipping broken code and waiting for support calls. The only way to do that is to
write those tests, then run them.
The lifecycle of a TestCase
JUnit runs every test method in the same way. It enumerates all test methods in a test
class (here, our LessSimpleTest class) and creates an instance of that class for each
test method, passing the method name to the constructor. Then, for every test
method, it runs the following routine:
public void runBare() throws Throwable {
setUp();
try {
runTest();
}
finally {
tearDown();
}
}

That is, it calls the method public void setUp(), runs the test method through
some introspection magic, and then calls public void tearDown(). The results
are forwarded to any classes that are listening for results.
You can add any number of test methods to a TestCase, all beginning with the
prefix test. Methods without this prefix are ignored. You can use this trick to turn
off tests or to write helper methods to simplify testing. JUnit 4.0 and TestNG let you
INTRODUCING JUNIT

89

use Java 5 annotations to mark the tests to run, but JUnit 3.8.x simply tests every
method beginning with the word test.
Test methods can throw any exception they want: there’s no need to catch exceptions and turn them into assertions or failures. What you do have to make sure of is
that the method signature matches what’s expected: no parameters and a void return
type. If you accidentally add a return type or an argument, the method is no longer
a test.
TIP

If you have an IDE that has macros or templates, write a template for a
testcase. For example, with IntelliJ IDEA you can map something like
“test” to:
public void test$NAME$() throws Throwable {
$END$
}

This creates a stub test method and prompts us to fill in the name. Declaring that it throws an Exception lets our test throw anything. Eclipse ships
with a similar template predefined.

To create or configure objects before running each test method, you should override
the empty TestCase.setUp method and configure member variables or other
parts of the running program. You can use the TestCase.tearDown method to
close any open connections or in some way clean up the machine, along with try {}
finally {} clauses in the methods themselves. If, for example, we wanted to have a
configured Event instance for each test, we could add the member variable and then
create it in an overridden setUp() method. Because an instance of the class is created for every test method before any of the tests are run, you can’t do setup work in
the constructor, or cleanup in any finalizer.
To summarize: the setUp and tearDown methods are called before and after
every test method and should be the only place where you prepare for a test and clean
up afterwards.
Once the tests are written, it’s time to run the tests. This is where Ant comes
into play.
4.4.4

Adding JUnit to Ant
Ant has a task to run the JUnit tests called, not surprisingly, . This is an
optional task. Ant has three categories of tasks.
• Core tasks are built into ant.jar and are always available.
• Optional tasks are tasks that are supplied by the Ant team, but are either viewed
as less important or they depend on external libraries or programs. They come in the
ant-optional.jar or a dependency-specific JAR, such as ant-junit-jar.
• Third-party tasks are Ant tasks written by others. These will be introduced in
chapter 9.

90

CHAPTER 4

TESTING WITH JUNIT

ANT 1.7

The task is an optional task, which depends upon JUnit’s JAR file to run
the tests.
Older versions of Ant required junit.jar to be in Ant’s classpath by placing it
in a directory that Ant loaded at startup. Ant 1.7 has changed this, so that now a copy
of JUnit in the classpath that you set up for compiling and running the test code is all
that’s needed. Unfortunately, a lot of existing build files assume that the junit.jar
is always on Ant’s classpath, so they don’t bother to add it. Given how important
JUnit is, you may as well copy it and place it where Ant can load it. This is a good
time to introduce how Ant adds libraries to its classpath.
When you type ant at the command line, it runs Ant’s launcher code in antlauncher.jar. This sets up the classpath for the rest of the run by
• Adding every JAR listed in the CLASSPATH environment variable, unless the
-noclasspath option is set
• Adding every JAR in the ANT_HOME/lib directory
• Adding every JAR in ${user.home}/.ant/lib, where ${user.home} is
the OS-specific home directory of the user, unless the -nouserlib option is set
• Adding every JAR in every directory listed on the command line with the
-lib option
The key thing to know is that all JAR files in ANT_HOME/lib and ${user.home}/
.ant/lib are added to Ant’s classpath automatically. If junit.jar, or any other
library is placed there, then it’s available to Ant and its tasks. It can also be picked up
by any build file that uses Ant’s own classes when compiling or running programs.
All we need to do then is download junit.jar, stick it in the correct place and
have Ant’s task pick it up. This is something that can be done by hand, or
it can be done by asking Ant to do the work itself.

ANT 1.7

Interlude: how to automatically fetch JAR files for use with Ant. If you’re online
and a proxy server is not blocking outbound HTTP connections, change to Ant’s
home directory, then type:
ant -f fetch.xml all

This code runs a special build file that fetches all the libraries that Ant needs and saves
them in ANT_HOME/lib. If you need to save the JAR files in your personal .ant/
lib directory, add the -Ddest=user clause. Welcome to the world of automated
JAR-file downloading!
If you work in a security-sensitive organization, you shouldn’t download and
install files without first authenticating them. You might even want to consider
downloading the source and building the files yourself.
To see what is on Ant’s classpath, type:
ant -diagnostics

INTRODUCING JUNIT

91