4.7 Test Code Organization

     

4.7 Test Code Organization

As a project grows in size , organizing the files containing production and test code becomes an issue. Although keeping the test and production code in the same directory is the simplest solution, it is better to have a clean separation between the two categories of code. This strategy helps avoid build complications that occur when a directory contains some code that should be linked into the production application, and some that should not. Including the test code in the delivered application is undesirable because it unnecessarily increases the size of the delivery, and also because the tests may expose behavior or design details that the developer meant to keep "under the hood."

Organizing the code is a language-specific concern. In Java, the directory path to a source file parallels its package membership. The need to test protected interfaces means that unit tests should belong to the same package as the production classes they test, so they must have the same directory path . This can be done by creating separate but parallel hierarchies for the production and test code.

Figure 4-1 shows how the source code for the final version of the virtual library application is organized. There are three Java packages, com.utf.library , com.utf.library.gui , and com.utf.library.xml .

Figure 4-1. Organization of production and test code
figs/utf_0401.gif

The production and test code are located in separate directories, src and test , which are located under the project's top level DEVROOT directory. For example, the production class Library resides in the directory src/com/utf/library , and the test class LibraryTest is in test/com/utf/library . The test classes' package names parallel the production classes' package names , so the test classes can access and test protected behavior of the production code. Since the code is in separate directory trees, it is simple to build and run only the production or test code as desired.

For many other languages, an effective way to organize the code is to place all test code in a subdirectory named test within each production code directory. This arrangement keeps the test code separate, but makes linking it to the production code simple.

     

4.8 Mock Objects

Applications often use interfaces to external objects such as databases, web servers, network services, or hardware devices. Sometimes you must write and test code to interface with objects before they are actually available. Even when the external object is available in the development environment, using it in testing may involve lots of time-consuming , fragile set-up effort, such as loading test data, running services, or placing hardware in a known state. Mock objects are a way of dealing elegantly with this type of situation.

A mock object is a simulation of a real object. Mocks implement the interface of the real object and behave identically with it, to the extent necessary for testing. Mocks also validate that the code that uses them does so correctly. To pass the mock's validation, other objects must call the correct methods , with the expected parameters, in the expected order. A test object that simply stands in for a real object without providing such verification is not a mock; it is a stub.

Databases are commonly mocked objects. Code that interfaces to a database clearly is important to test. To be tested realistically , the code must be able to perform database operations such as opening and closing connections, reading and writing data, and performing transactions. However, running a live database in the development environment can be a pain. Tests often require that the database is in a specific state or that it contains a specific set of test data. If multiple developers run tests simultaneously , their database operations may interfere.

Mocking the database makes having an actual database unnecessary for testing. The mock has the same interface as the actual database object and the same behavior from the perspective of the client software, but it doesn't need to actually contain anything but a minimal implementation and possibly some test data. Once the database mock is created, it becomes much simpler to write tests that assume that the database is in various states. Testing becomes faster and easier without the overhead of interfacing with an actual database engine.

To illustrate this, let's create a mock object representing a database connection object. An interface called DBConnection represents a database connection, as shown in Example 4-14.

Example 4-14. The interface DBConnection, representing a database connection
 DBConnection.java

public interface  DBConnection  {

   void connect( );

   void close( );

   Book selectBook( String title, String author );

} 

The class LibraryDB retrieves Book s from a database using DBConnection . It is shown in Example 4-15.

Example 4-15. The database interface LibraryDB
 LibraryDB.java

public class  LibraryDB  {



   private  DBConnection  connection;



   public  LibraryDB  ( DBConnection c ) {

      connection = c;

   }



   Book  getBook  ( String title, String author ) {

      connection.connect( );

      Book book = connection.selectBook( title, author );

      connection.close( );

      return book;

  }

} 

We would like to build a unit test for LibraryDB , but we don't have an actual database yet. So, we'll mock DBConnection as shown in Example 4-16.

Example 4-16. The mock object MockDBConnection
 MockDBConnection.java

public class  MockDBConnection  implements  DBConnection  {



   private boolean  connected  = false;

   private boolean  closed  = false;



   public void  connect( )  { connected = true; }

   public void  close( )  { closed = true; }

   public Book  selectBook  ( String title, String author ) {

      return null;

   }



   public boolean  validate( )  {

      return connected && closed;

   }



} 

MockDBConnection implements the public interface of DBConnection , so it can be used in the interface's place. MockDBConnection uses the attributes connected and closed to record that the connect( ) and close() methods have been called. The validate( ) method verifies the connection's state by checking these flags. So, the expectation set by the mock is that code using DBConnection must call both connect( ) and close( ) .

The test class LibraryDBTest is shown in Example 4-17.

Example 4-17. The test class LibraryDBTest
 LibraryDBTest.java

import junit.framework.*;

import java.util.*;



public class  LibraryDBTest  extends TestCase {



   public void  testGetBook( )  {

      MockDBConnection mock =  new MockDBConnection( )  ;

      LibraryDB db =  new LibraryDB( mock )  ;

      Book book = db.getBook( "Cosmos", "Carl Sagan" );  assertTrue( mock.validate( ) )  ;

   }

} 

The test method testGetBook( ) creates an instance of MockDBConnection , uses it to construct a LibraryDB , and then calls the LibraryDB method getBook( ) . The success of the test depends on the result of the mock's validate( ) function. If the mock is in the expected state, its validation succeeds and the test passes . The mock object verifies the expected sequence of calls to the database connection and validates that LibraryDB is interacting with it correctly. It also allows LibraryDB and DBConnection to be tested without an actual database.

More sophisticated mock objects go beyond simply setting flags for each method called by recording the arguments provided for method calls, the order of calls, and other details of the method's state. In this way, mock objects can perform sophisticated validation of interobject interactions.

Mock objects are a deep topic, covered by numerous web sites, books, and online groups. Also, a variety of tools are available to support mock object development for various domains and languages, including EasyMock, jMock, and MockRunner.