Chapter 2. Getting Started: Tutorial

     

Chapter 2. Getting Started: Tutorial

Software concepts are best explained by example. In this tutorial, you will set up a simple unit test framework and use it to help build a basic application. Following the primary rule of TDD, every change to the code is preceded by a unit test.

Why build our own test framework, instead of starting with one of the xUnits? The xUnit test frameworks are powerful tools. They not only support writing unit tests, running them, and reporting the results, but also include test classes, helper code, test runners, and utilities. Such features minimize the amount of code required to write a unit test and maximize your ability to test complex code. They include much more than the minimum needed to build unit tests.

The core functionality of running tests and reporting the results is fundamentally simple. Developers working in cross-platform environments, using older compilers or uncommon languages or needing closer control over how unit tests and their results are handled may not be able to use the xUnits or want to invest the time to set them up. The proliferation of very basic unit test frameworks available online demonstrates the popular belief that "simpler is better" when it comes to unit test frameworks. Most importantly, creating your own framework clearly demonstrates how unit tests work and how straightforward the unit test framework concept really is.

The example code is given in Java. Appendix A contains the C++ version. The code can be found on the CD accompanying this book in the directory /examples/chapter2 . Consider entering the code in this chapter by hand as if you were coding it from scratch. It's an illuminating exercise that will help you to understand how quick and easy it is to set up and start using a unit test framework.

This tutorial assumes that you have a Java runtime environment and compiler installed. Sun's javac compiler is recommended, as is the GNU gcj Java compiler. Versions of both compilers are readily available for most platforms.

The step-by-step procedures given here assume that you are compiling and running the code from the command line. If you are using a graphical Integrated Development Environment (IDE), the details of how you build and run the example code will differ .

     

2.1 Outline of an Application: the Virtual Library

This book presents an increasingly complex series of code examples to illustrate unit test framework usage. The examples fit into an overall system concept. Your mission, should you choose to accept it, is to build a system for managing a library full of books. Books will have the attributes you might expect, such as title and author. Users of the system will need to be able to perform a variety of library operations: adding new books, searching for books, checking them in and out of the library, and so forth.

     

2.2 Example 1: Create a Book

For the first example, we will create a representation of a book and its title. Since we'll do test-first development, we need to set up a unit test framework prior to writing any code for the book class. This test framework serves both as the foundation for the example's unit tests and also as an illustration of just how simple a functional test framework can be. Building it is Step 0.

The subsequent steps are the usual three steps in the TDD cycle. Step 1 is to write a unit test to verify that a book has been created. At first, the unit test will fail, because the functionality to create a book does not yet exist. Step 2 is to build the functionality to create a book. In Step 3, the test succeeds, proving that the functionality works and providing an example of how to use it.

2.2.1 Step 0: Set Up the Unit Test Framework

The unit test framework initially is built on a single class, UnitTest , shown in Figure 2-1.

Figure 2-1. The class UnitTest
figs/utf_0201.gif

The source code for UnitTest is given in Example 2-1.

Example 2-1. The base class UnitTest
 UnitTest.java

public abstract class  UnitTest  {



   protected static int  num_test_success  = 0;



   public abstract void  runTest( )  throws Exception;



   protected void  assertTrue( boolean condition, String msg )  throws Exception {

      if (!condition)

         throw new Exception(msg);

      num_test_success++;

   }



} 

The class UnitTest is abstract because its purpose is to be the parent class for actual unit tests. It contains a static integer member, num_test_success , which keeps track of the number of successful tests. Descendant classes override the method runTest( ) to run actual tests. The method assertTrue() tests a condition. If the condition is TRUE , the successful test counter is incremented. If it is FALSE , an Exception is thrown containing a message string associated with the condition.

Compile UnitTest with the command javac UnitTest.java (or your compiler's equivalent command). Believe it or not, you now have a simple but functional test framework with which to start building tests.

You could look at this unit test framework and ask, "How is something that can be written in 10 lines of code worth an entire book?" Unit test frameworks are fundamentally simple tools that can be used in sophisticated ways. The xUnit frameworks represent significantly more complicated and powerful pieces of software than the basic framework used in this example. In the subsequent chapters, we will get into the features of more advanced unit test frameworks and how they save coding effort and enable building more complex tests.

2.2.2 Step 1: Create a Unit Test

Now that you have built your simple unit test framework, it's time to create a unit test. The unit test will fail because the functionality it tests has not been built. Remember the TDD process: if your tests do not fail initially, then you are not writing good tests. The test failure also demonstrates that the framework works as expected.

Before writing the first test, take a moment to decide what you want the new code to do and how to test whether it succeeds. We want to represent a book and its title, which sensibly is done with a class named Book having a title attribute. So, we will create a unit test that creates an instance of Book and checks its title .

Example 2-2 shows the implementation of the first unit test, BookTest .

Example 2-2. The unit test class BookTest
 BookTest.java

public class  BookTest  extends UnitTest {



   public void  runTest( )  throws Exception {

      Book book = new Book("Dune");

      assertTrue(book.title.equals("Dune"), "checking title");

   }



} 

BookTest is very simple. It creates a book, giving the title as an argument to the constructor. It then tests that the value of the attribute title has been set correctly. The string checking title describes the test condition.

Compile this new class. The compiler will inform you that it does not know about a class named Book . So, create the most basic implementation of Book that will allow everything to compile, as shown in Example 2-3.

Example 2-3. The class Book
 Book.java

public class  Book  {



   public String title = "";



   Book(String title) {}



} 

Someone who cares about software design would have a problem with this code. Making the title attribute public is not good; it would be better to make it private and provide an accessor function such as getTitle( ) to obtain its value. However, adding the accessor now would create two methods that should be unit tested: the constructor and the accessor. This change should wait until the current change is done and tested .

Book and BookTest can now compile. Our first unit test is now built. We still need to run the test to see whether it succeeds or fails. One additional new piece of code is necessary. The class TestRunner runs BookTest and reports success or failure. Example 2-4 gives the implementation of TestRunner .

Example 2-4. The class TestRunner
 TestRunner.java

public class  TestRunner  {



   public static void  main(String[] args)  {

      TestRunner tester = new TestRunner( );

   }



   public TestRunner( ) {

      try {

         UnitTest test = new  BookTest  ( );

         test.  runTest( )  ;

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

      }

      catch (Exception e) {

         e.printStackTrace( );

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

      }

   }

} 

TestRunner contains the main() method for the test framework. When an instance of TestRunner is created, it creates a BookTest and calls its runTest( ) method. If there is no error, success is reported . If an exception is thrown, TestRunner reports the location of the failure from the exception stack trace.

Compile the new code and run it using java TestRunner . You should get the following results:

 FAILURE!

java.lang.Exception: checking title

        at UnitTest.assertTrue(UnitTest.java:10)

        at BookTest.runTest(BookTest.java:5)

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

        at TestRunner.main(TestRunner.java:4) 

A failure is reported. The test description is printed, followed by the stack trace showing where the failure occurred.

Congratulations! You have produced a test failure. This failure is a success of the TDD process. The unit test BookTest has done its job, reporting that the functionality being tested is not implemented. The simple framework has performed its role, running the test and reporting the results, including the location and description of the failure. We now have a working unit test framework.

A semantic distinction is drawn between failures and errors in unit testing. A failure is a unit test reporting that a test condition has evaluated to false. If you're not producing failures, you're not writing good tests. An error is an unexpected problem, such as an uncaught exception. Errors may happen, but producing them is not a goal of the TDD process.

2.2.3 Step 2: Create a Book

In the previous step, we wrote the first unit test, BookTest . In order to get BookTest to compile, we also created a basic implementation of the class Book . When run, BookTest still fails, because Book does not yet contain the functionality being tested. In this step, we add the necessary code to get the test to succeed. As compared to Step 1, the changes required at this point are very minor.

The class Book is modified so that the title attribute is set in the constructor, as shown in Example 2-5.

Example 2-5. The class Book with title attribute set by the constructor
 Book.java

public class Book {



   public String title = "";



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



} 

2.2.4 Step 3: Test Again

The final step is to rebuild the code, re-run the unit test, and see whether the changes produce the desired results.

Compile the code and run it using java TestRunner . You should see the following result:

 SUCCESS! 

Mission accomplished! Creating a class representing a book with a title attribute is a simple task that most developers could accomplish in a few minutes, without feeling the need for a unit test to validate it. However, we accomplished much more than that in this exercise. We created a simple unit test framework, built a unit test, and validated that the framework behaves as expected in both failure and success cases. The initial unit test, although a trivial validation of the class Book , is important as a test of the framework itself.

From the formal software design perspective, the class architecture we've just built is shown in Figure 2-2.

Figure 2-2. Class diagram for the basic unit test framework
figs/utf_0202.gif