3.3 The xUnit Architecture

     

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
figs/utf_0301.gif

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 TestCase s and report the results. Figure 3-2 shows TextTestRunner .

Figure 3-2. The class TextTestRunner
figs/utf_0302.gif

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 Book s to be added and to get the number of Book s 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 Book s to the Library , then uses getBook( ) to verify that the additions succeeded. The method testLibrarySize( ) also adds two Book s, 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 Book s. The new method getNumBooks( ) returns the number of Book s 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
figs/utf_0303.gif

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 TestFixture s 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 Book s.

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
figs/utf_0304.gif

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 TestCase s are run. TestCase s 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 TestSuite s and TestCase s.

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
figs/utf_0305.gif

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.