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
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.
|