Sunday, October 25, 2009

3.3 The xUnit Architecture











 < Day Day Up > 







3.3 The xUnit Architecture





The xUnits

all have the same basic architecture. This

section describes the xUnit fundamentals, using JUnit as the

reference example, since it is the most widely used of the xUnits.

The other xUnits vary in their implementation details, but follow the

same pattern and generally contain the same key classes and concepts.

The key classes are TestCase,

TestRunner, TestFixture,

TestSuite, and TestResult.





The architecture diagrams in this section leave out some methods and

other design details for clarity and represent the generic xUnit

design, not that of JUnit.







3.3.1 TestCase





xUnit's



most elemental class is

TestCase, the base class for a unit test. It is

shown in Figure 3-1.







Figure 3-1. The abstract class TestCase, the parent of all xUnit unit tests







All unit tests are inherited from TestCase. To

create a unit test, define a test class that is descended from

TestCase and add a test method to it. Example 3-1 shows the unit test

BookTest.







Example 3-1. BookTest, a test built on TestCase


BookTest.java

import junit.framework.*;



public class BookTest extends TestCase {



public void testConstructBook( ) {

Book book = new Book("Dune");

assertTrue( book.getTitle( ).equals("Dune") );

}



}









The test method testConstructBook() uses assertTrue() to

check the value of the Book's

title. Test conditions always are evaluated by the

framework's assert methods. If a condition evaluates

to TRUE, the framework increments the successful

test counter. If it is FALSE, a test failure has

occurred and the framework records the details, including the

failure's location in the code. After a failure, the

framework skips the rest of the code in the test method, since the

test result is already known.





BookTest tests the class

Book, shown in

Example 3-2.







Example 3-2. The class Book


Book.java

public class Book {



private String title = "";



Book(String title) { this.title = title; }



String getTitle( ) { return title; }

}









This is the Book class developed in Chapter 2, with a few changes. The

title attribute is now private

and the accessor function getTitle( ) is added.





BookTest can be run by adding a main() method that calls the test method, as shown

in Example 3-3.







Example 3-3. BookTest with changes allowing it to be run


BookTest.java

import junit.framework.*;



public class BookTest extends TestCase {



public void testConstructBook( ) {

Book book = new Book("Dune");

assertTrue( book.getTitle( ).equals("Dune") );

}



public static void main(String args[]) {

BookTest test = new BookTest( );

test.testConstructBook( );

}



}









Compiling and running BookTest produces a

disappointing lack of output and not much confidence that anything

actually happened.





> javac BookTest.java

> java BookTest

>







For the commands to work as shown, junit.jar and

the directory containing the test classes must be in the

Java

classpath.





The results are more interesting if BookTest is

made to fail by changing the assertTrue( )

condition to FALSE.





> java BookTest

Exception in thread "main" junit.framework.AssertionFailedError

at junit.framework.Assert.fail(Assert.java:47)

at junit.framework.Assert.assertTrue(Assert.java:20)

at junit.framework.Assert.assertTrue(Assert.java:27)

at BookTest.testConstructBook(BookTest.java:7)

at BookTest.main(BookTest.java:12)







You can see that the unit test framework is doing its job, running

the test and reporting the test failure. This demonstrates that an

xUnit framework can be used in a very simple and straightforward way.

Basic unit tests can be built on TestCase without

any additional knowledge of the framework. However, the xUnits have

other, more useful functionality to offer. One of the most valuable

pieces is TestRunner.









3.3.2 TestRunner





A TestRunner reports details about the test results

and simplifies the test. It is a fairly complex object that, in

JUnit, comes in three flavors: the

AWT

TestRunner, the Swing TestRunner, and

the textual TestRunner (cleverly named

TextTestRunner.) Their purpose is to run one or more

TestCases and report the results. Figure 3-2 shows TextTestRunner.







Figure 3-2. The class TextTestRunner







The important methods of

TextTestRunner

are run( ), which

gives it a test to run, and main( ), which makes

TextTestRunner a runnable class.

TextTestRunner will be run with the test class

BookTest as its argument. It will find the test

method testConstructBook and run it.





You can remove the main( ) method in

BookTest, since you no longer need it

to run the test. Example 3-4 shows the refactored

BookTest.







Example 3-4. BookTest made simple again


BookTest.java

import junit.framework.*;



public class BookTest extends TestCase {



public void testConstructBook( ) {

Book book = new Book("Dune");

assertTrue( book.getTitle( ).equals("Dune") );

}



}









BookTest is reduced back to its essentials. Now,

use

TextTestRunner to run BookTest:





> java junit.textui.TestRunner BookTest

.

Time: 0.01



OK (1 test)







Using the TestRunner not only takes unnecessary

code out of BookTest, but also provides a nice

report of how many tests were run and how long they took.





Test classes often have multiple test methods.

TestRunner will find all of the test methods that

have names starting with test and run them. Example 3-5 shows BookTest with a

second test method added. The new test validates a

Book's author.







Example 3-5. BookTest with a second test method


BookTest.java

import junit.framework.*;



public class BookTest extends TestCase {



public void testConstructBook( ) {

Book book = new Book("Dune", "");

assertTrue( book.getTitle( ).equals("Dune") );

}



public void testAuthor( ) {

Book book = new Book("Dune", "Frank Herbert");

assertTrue( book.getAuthor( ).equals("Frank Herbert") );

}



}









The author attribute and its accessor

function getAuthor( ) are added to

Book, as shown in Example 3-6.







Example 3-6. Book with an author attribute


Book.java

public class Book {



private String title = "";

private String author = "";



Book(String title, String author) {

this.title = title;

this.author = author;

}



public String getTitle( ) { return title; }

public String getAuthor( ) { return author; }

}









Running BookTest shows that the framework now is

running two tests:





> java junit.textui.TestRunner BookTest

..

Time: 0.01



OK (2 tests)







A dot is printed when each test is run as a progress indicator. The

test output concludes with the number of tests and the elapsed time.





Most of the xUnits include a GUI

TestRunner to

provide enhanced visual feedback on the test results. The results are

highlighted in green if all the tests succeed, or in red if there is

a test failure. (This is the origin of the terms green

bar


and red

bar
. The TDD cycle is sometimes described as

" Red-Green-Refactor"

because of this. First, implement a new test that fails, causing a

red bar; then, make the simplest possible code change that restores

the green bar; finally, refactor the possibly ugly code that was

introduced.) The chapters later in this book that describe specific

versions of xUnit show screenshots of their

TestRunner GUIs.









3.3.3 TestFixture





To

explain

test fixtures, another important xUnit concept, a more complex unit

test example is useful. Functionality will be added to the

Library class from Chapter 2

to allow multiple Books to be added and to get the

number of Books the Library

class contains. Example 3-7 gives an initial version

of the unit test LibraryTest that tests these new

features.







Example 3-7. Initial version of LibraryTest


LibraryTest.java

import junit.framework.*;

import java.util.*;



public class LibraryTest extends TestCase {



public void testAddBooks( ) {

Library library = new Library( );

library.addBook(new Book("Dune", "Frank Herbert"));

library.addBook(new Book("Solaris", "Stanislaw Lem"));

Book book = library.getBook( "Dune" );

assertTrue( book.getTitle( ).equals("Dune") );

book = library.getBook( "Solaris" );

assertTrue( book.getTitle( ).equals("Solaris") );

}



public void testLibrarySize( ) {

Library library = new Library( );

library.addBook(new Book("Dune", "Frank Herbert"));

library.addBook(new Book("Solaris", "Stanislaw Lem"));

assertTrue( library.getNumBooks( ) == 2 );

}



}









Two test methods are implemented. The method

testAddBooks( ) adds two Books

to the Library, then uses getBook(

)
to verify that the additions succeeded. The method

testLibrarySize( ) also adds two

Books, then checks that getNumBooks(

)
returns "2".





Example 3-8 shows the new version of

Library with the additional

functionality to pass the tests.







Example 3-8. New version of Library


Library.java

import java.util.*;



public class Library {



private Vector books;



Library( ) {

books = new Vector( );

}



public void addBook( Book book ) {

books.add( book );

}



public Book getBook( String title ) {

for ( int i=0; i < books.size( ); i++ ) {

Book book = (Book) books.elementAt( i );

if ( book.getTitle( ).equals(title) )

return book;

}

return null;

}



public int getNumBooks( ) {

return books.size( );

}



}









Library now uses a Vector to contain a

collection of Books. The new method

getNumBooks( ) returns the number of

Books in the collection. The methods

addBook( ) and getBook( ) add

and retrieve a Book from the collection.





When you use TextTestRunner to execute

LibraryTest, both test methods succeed:





> java junit.textui.TestRunner LibraryTest

..

Time: 0.05



OK (2 tests)







LibraryTest has a number of problems. First and

foremost, the amount of code duplication between the two test methods

is bothersome. Both of them create a test Library

and add two books to it. Second, another concern is what will happen

if one of the asserts fails. The rest of the code in the test method

will not be executed and any objects created will not be cleaned up.

In Java, the garbage collector will

deallocate objects automatically, but

often unit tests use

objects or resources that must be

explicitly closed or deleted.





One way to take care of the code duplication is to make

the test Library a member of

LibraryTest and have the first test initialize it

and add the initial two elements. The second test could assume that

the first test succeeded, run its tests, and then clean up.

Unfortunately, this solution introduces more potential problems. If

the first test fails, the second also may fail because its initial

conditions are wrong, even though there may be nothing wrong with the

functionality it tests. The second test will always fail unless the

first one is run before it, so they cannot be run separately or in

reverse order. Furthermore, failure of either test is likely to

result in things not getting cleaned up.





In general, well-written unit tests exhibit

isolation . An

isolated test doesn't depend in any way on the

results of other tests. To ensure isolation, tests should not share

objects that change. Tests that have



interdependencies are

coupled. In LibraryTest, if

one of the test methods assumed that the other test left the

Library in a certain state, it would be a classic

example of test coupling.





The xUnit architecture helps to ensure test isolation with

test fixtures. A test fixture is a

test environment used by multiple tests. It is implemented as a

TestCase with multiple test methods that share

objects. The shared objects represent the common test environment.

Figure 3-3 shows the relationship between a

TestFixture and a

TestCase.







Figure 3-3. TestFixture and its child TestCase







Every TestCase is implicitly a

TestFixture, although it may not act as one. The

TestFixture behavior comes into play when multiple

test methods have objects in common. The setUp( )

method is called prior to each test method, establishing the initial

environment for the test. The tearDown( ) method

is always called after each test method to clean up the test

environment, even if there is a failure. Thus, although the tests use

the same objects, they can make changes without the possibility of

affecting the next test.





The TestFixture behavior effectively creates and

destroys the test class each time one of its test methods is called.

This may incur a performance penalty, but it is important to

guarantee that the tests are isolated.





Incidentally, some xUnits (such as CppUnit) have an actual class or

interface named TestFixture from which

TestCase is descended, while some (JUnit) just

allow TestCase to act as a

TestFixture.





Writing tests as TestFixtures has a number of

advantages. Test methods can share objects but still run in

isolation. Test coupling is minimized. Test methods that share code

can be grouped together in the same TestFixture.

Code duplication between tests is reduced. The cleanup code is

guaranteed to run whether a test succeeds or fails. Finally, the test

methods can be run in any order, since they are isolated. Example 3-9 shows

LibraryTest implemented as a

TestFixture. In this example, the test

fixture's shared environment contains an instance of

Library with two Books.







Example 3-9. LibraryTest implemented as a TestFixture


LibraryTest.java

import junit.framework.*;

import java.util.*;



public class LibraryTest extends TestCase {



private Library library;



public void setUp( ) {

library = new Library( );

library.addBook(new Book("Dune", "Frank Herbert"));

library.addBook(new Book("Solaris", "Stanislaw Lem"));

}



public void tearDown( ) {

}



public void testGetBooks( ) {

Book book = library.getBook( "Dune" );

assertTrue( book.getTitle( ).equals( "Dune" ) );

book = library.getBook( "Solaris" );

assertTrue( book.getTitle( ).equals( "Solaris" ) );

}



public void testLibrarySize( ) {

assertTrue( library.getNumBooks( ) == 2 );

}



}









The stylistic improvements over the previous version of

LibraryTest are apparent: the code duplication is

gone, the test methods contain only statements specifically related

to the test conditions, and the tests are easier to understand.





Note that the test method previously named testAddBooks() is renamed testGetBooks( ) to more

accurately describe what it's doing.





When LibraryTest is run, the sequence of function

calls is:





setUp( )

testGetBooks( )

tearDown( )

setUp( )

testLibrarySize( )

tearDown( )







The calls to setUp( )

and tearDown( )

initialize and deinitialize the test fixture each time a test method

is called, thus isolating the

tests.









3.3.4 TestSuite





So



far, this review of xUnit has

focused on writing single unit test classes, sometimes with multiple

test methods. What about testing with multiple unit test classes?

After all, each production object should have a corresponding unit

test.





xUnit contains a class for aggregating unit tests called

TestSuite. TestSuite is closely

related to TestCase, since both are descendants of

the same abstract class, Test. Figure 3-4 shows the

Test interface and how

TestSuite and TestCase

implement it.







Figure 3-4. TestSuite, TestCase, and their parent interface Test







The interface Test contains the run() method that the framework uses to

run tests and collect their results. Since

TestSuite implements run(), it can be run just like a

TestCase. When a TestCase is

run, its test methods are run. When a TestSuite is

run, its TestCases are run.

TestCases are added to a

TestSuite using the addTest()

method. Since a TestSuite is itself a

Test, a

TestSuite can contain other

TestSuites, allowing the intrepid developer to

build hierarchies of TestSuites and

TestCases.





Example 3-10 shows a

TestSuite-derived class named

LibraryTests that contains both

BookTest and LibraryTest.







Example 3-10. An instance of TestSuite named LibraryTests


LibraryTests.java

import junit.framework.*;



public class LibraryTests extends TestSuite {



public static Test suite( ) {

TestSuite suite = new TestSuite( );

suite.addTest(new TestSuite(BookTest.class));

suite.addTest(new TestSuite(LibraryTest.class));

return suite;

}



}









A TestSuite is created for each of the test

classes and added to LibraryTests. This is a quick

way to add all of the test methods to the test suite at once. The

addTest( ) method also may be used to add test

methods to a test suite individually, as shown here:





suite.addTest(new LibraryTest("testAddBooks"));







To be used this way, an instance of TestCase must

have a constructor that takes a string argument and invokes its

parent's constructor. The string argument specifies

the name of the test method to run.





You can run instances of TestSuite using a

TestRunner just as you would run a

TestCase. The

TestSuite's static method

suite( ) is

called to create the suite of tests to run.





> java junit.textui.TestRunner LibraryTests



....

Time: 0.06



OK (4 tests)







The results show that both of the test methods from the

LibraryTest and BookTest unit

test classes have been run, for a total of four

tests.









3.3.5 TestResult





As

shown in

the discussion of the Test

interface, TestResult is the parameter to

Test's run( )

method. The immediate goal of running unit tests, in a literal sense,

is to accumulate test results. The class

TestResult serves this purpose. Each time a test

is run, the TestResult object is passed in to

collect the results. Figure 3-5 shows

TestResult.







Figure 3-5. The class TestResult, used to collect test outcomes







TestResult is a simple object. It counts the tests

run and collects test failures and errors so the

framework can report them. The failures and errors include details

about the location in the code where they occurred and any associated

test descriptions. The information printed for the

BookTest failure at the beginning of this chapter

is typical.



















     < Day Day Up > 



    No comments: