2.3 Example 2: Create a Library

     

2.3 Example 2: Create a Library

For the second example, we'll add additional functionality to the library application. The new features will allow us to create a library, add a book to it, and get a book from it. Along the way, we also will add a few features to the unit test framework.

Consider the minimum new code that will provide what is necessary. Creating a library is easy. We can instantiate an empty class called Library and be done. Adding a book to the library is a feature with a little more to think through. We have a class Book , and the fact that we can add a Book to a Library suggests how a Library should work. A Library contains Book s. The ability to get a book from the library reinforces this idea.

Let's create a unit test that adds a Book to a Library and then gets the Book back out again, verifying that the Library contains the Book .

2.3.1 Step 1: Test adding a Book to a Library

The class LibraryTest is the initial unit test for the Library class. Its implementation is shown in Example 2-6.

Example 2-6. Initial version of LibraryTest
 LibraryTest.java

public class  LibraryTest  extends UnitTest {



   public void  runTest  ( ) throws Exception {

      Library library = new Library( );

      Book expectedBook = new Book( "Dune" );

      library.addBook( expectedBook );

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

      assertTrue( actualBook.title.equals("Dune"), "got book" );

   }



} 

The test creates a Library and a Book , adds the Book to the Library , then gets the Book from the Library and asserts that the expected Book was found.

Additional code must be added to TestRunner to run LibraryTest , as shown in Example 2-7.

Example 2-7. TestRunner modified to run LibraryTest
 TestRunner.java

public class TestRunner {



   public static void main(String[] args) {

      TestRunner tester = new TestRunner( );

   }



   public TestRunner( ) {

      try {

         BookTest test = new BookTest( );

         test.runTest( );

         LibraryTest test2 = new LibraryTest( );

         test2.runTest( );

         System.out.println("SUCCESS!");

      }

      catch (Exception e) {

         System.out.println("FAILURE!");

         e.printStackTrace( );

      }

      System.out.println( UnitTest.getNumSuccess( )

         + " tests completed successfully" );

   }

} 

Now that more than one unit test is being run, it's useful to report the value of the test success counter. To obtain this value, the accessor function getNumSuccess() is added to the class UnitTest , as shown in Example 2-8.

Example 2-8. UnitTest with accessor function getNumSuccess
 UnitTest.java

public abstract class UnitTest {



   protected static int num_test_success = 0;



   public abstract void runTest( ) throws Exception;



   public static int getNumSuccess( ) 

      { return num_test_success; }



   protected void assertTrue(boolean condition, String msg) 

      throws Exception {

      if (!condition)

         throw new Exception(msg);

      num_test_success++;

   }



} 

So far, the code will not compile because there's no class named Library . Let's create the most basic implementation that will allow LibraryTest to compile, as shown in Example 2-9.

Example 2-9. Initial version of the class Library
 Library.java

public class  Library  {



   Library( ) {}



   public void addBook( Book book ) {}



   public Book getBook( String title ) {

      return new Book("");

   }

} 

The code should now compile and run. The framework should report the failure of LibraryTest , as well as the success of BookTest :

 FAILURE!

java.lang.Exception: got book

        at UnitTest.assertTrue(UnitTest.java:13)

        at LibraryTest.runTest(LibraryTest.java:11)

        at TestRunner.<init>(TestRunner.java:12)

        at TestRunner.main(TestRunner.java:4)

1 tests completed successfully 

2.3.2 Step 2: Add a Book to a Library

Now it's time to add the functionality to make LibraryTest succeed. We should only have to change the Library class. If any other changes were necessary, it would suggest that the unit test relies on some behavior other than what Library provides.

You might already have a design in mind for Library that uses some kind of collection to store a set of Book s. You could start building it at this point. But consider this: Library can be made to pass LibraryTest without using a collection. Since we should not be building any functionality without first writing a unit test for it, implementing a collection of Book s is going too far. Stick to the principle of doing "the simplest thing that could possibly work."

Example 2-10 shows the Library class with new functionality to pass LibraryTest .

Example 2-10. Library with changes to pass LibraryTest
 Library.java

public class  Library  {



   private Book book;



   Library( ) {}



   public void addBook( Book book ) {

      this.book = book;

   }



   public Book getBook( String title ) {

      return book;

   }



} 

The Library class now contains a data member: a single Book . This may seem like cheating. After all, we certainly want a library to be able to hold more than one item. But LibraryTest tests only adding and retrieving a single Book , so the code given here is the minimum necessary to pass the test. Before implementing a Library that can contain multiple Book s, add a new unit test to LibraryTest to test that behavior.

2.3.3 Step 3: Check Unit Test Results

Compiling and running this code should demonstrate success for both of the unit tests:

 SUCCESS!

2 tests completed successfully 

The architecture of our unit test framework, unit tests, and production classes is shown in Figure 2-3.

Figure 2-3. Class diagram for the unit test framework and unit tests
figs/utf_0203.gif

The current implementation of Library is trivial. While writing the code to pass LibraryTest , we realized that it could be satisfied with a Library that contains a single book. So, the obvious unit test to write next is one that tests adding and retrieving multiple Book s from the Library . Often, adding one unit test and the corresponding functionality makes it clear what the next step should be. Since you are building a series of unit tests as you go and constantly validating the new code, you can be confident that everything you've built is working, and rapidly make changes. Sometimes you will find that a trivial implementation put in place just to get a unit test to pass stays in place for many iterations of the software. That's fine! This is the process working to save you from building unnecessary code.

You might already have decided there are flaws in Library and Book . For example, Library 's getBook( ) method returns an uninitialized Book if it is called before addBook( ) . The class Book has a public attribute, title , which should be made private and accessed with a get method. How should you take care of such problems? Write tests that fail because of these problems, then write code that fixes them. Every time you come up with a potential change to the design, there is a clear process for trying it out. Test first, then code, then test again.