4.11 New Library and Book Code

     

4.11 New Library and Book Code

Example 4-23 gives the code for the version of Book referenced in this chapter.

Example 4-23. The class Book
 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; }



} 

The code for the final version of Library is given in Example 4-24. It uses a Hashtable to store the collection of Book s.

Example 4-24. The class Library
 Library.java

import java.util.*;



public class  Library  {



   private Hashtable books;



   Library( ) {

      books = new Hashtable( );

   }



   public void addBook( Book book ) {

      books.put( book.getTitle( ), book );

   }



   public Book getBook( String title ) {

      return (Book)books.get( title );

   }



   public Book getBook( String title, String author ) {

      return (Book)books.get( title );

   }



   public Vector getBooks( String author ) {

      Vector auth_books = new Vector( );

      for ( Enumeration e = books.elements( ); e.hasMoreElements( ); ) {

         Book book = (Book)e.nextElement( );

         if ( book.getAuthor( ).equals(author) ) 

            auth_books.add( book );

      }

      return auth_books;   

   }



   public void removeBook( String title ) throws Exception {

      if ( books.remove( title ) == null )

         throw new Exception("Book not found");

   }



   public int getNumBooks( ) {

      return books.size( );

   }



   public void empty( ) {

      books.clear( );

   }



} 

     

Chapter 5. Unit Testing GUI Applications

Unit tests for ordinary software objects are easy to conceptualize. Objects have behaviors that are represented by methods and attributes. Tests elicit these behaviors to validate them. Unit testing of GUI objects is a different and more complex problem.

GUI objects are the graphical elements that make up the user interface of most software applications. They include windows , buttons , frames , text boxes, menus , and many other types of widgets. Even very simple applications often contain dozens of them. GUI objects usually have many behaviors, such as responding to mouse movements or clicks, displaying values, being shown, hidden, highlighted, disabled, and so forth. You usually build GUI applications from standard toolkits, such as Java's Swing, wxWindows for C++, or .NET's WinForms. Most of the GUI object behavior is provided, and you simply assemble the standard objects and write code only to implement the custom behaviors of their application.

Doing test-first development of such GUI code is challenging. It may not be hard to test the process of simply creating and displaying an object, such as a window. As soon as it becomes necessary to test responses to user actions such as keyboard entries or mouse clicks, the tests can become very complicated. It often takes a good deal of messy code to create and test a single GUI element as a standalone unit. Sometimes it is not even possible to design an automated test that verifies a specific visual GUI behavior, such as "the dialog box pops up modally showing the alert icon and the warning message in red." The test has no way of validating how a dialog would appear to a user. In general, trying to test application logic by simulating user interaction with the GUI is labor- intensive and error-prone .

Consider the pseudocode shown in Example 5-1, which represents a typical event-handling method in a class implementing a dialog window. The code responds to a mouse click on a button by getting values from the dialog's text fields, validating them, calculating a result, and updating a document. If the validation fails, an error dialog is shown. In either case, the text fields are cleared and the dialog is redrawn.

Example 5-1. A typical dialog event handler method
 void  handleEvent  ( ) {

   switch ( eventType ) {

      case (  buttonClick  ) :

         getTextFieldValues( );

         if (  validateValues  ( ) ) {  calculateResult  ( );  updateDocument  ( );

         } else { // validation failed  showErrorDialog  ( );

         }  clearTextFields  ( );

         redraw( );

   }

} 

How should this dialog be unit tested ? Each step in the event handler is a behavior that deserves its own unit test. The overall result of clicking on the button should be tested, as well as the error case when bad values are entered and a message dialog appears. Testing this will require complex GUI event scripting. For each behavior tested, the tests may have to create the dialog window, fill in text field values, fire a mouse click event, verify that the resulting window state is as expected, and close the dialog. The tests could be affected by unexpected events, such as someone clicking on the dialog while it is being tested.

So, how can GUI objects be tested effectively, in a way that supports test driven development? One answer is laid out in an influential paper about this problem: "The Humble Dialog Box" by Michael Feathers. It points out that building GUIs using the standard tools usually leads to GUI objects that contain all the application logic pertaining to their functionality. Such objects are hard to work with in the context of a test harness, and the contained application logic is hard to separate from the GUI behavior for testing. The way to solve this problem is by creating smart objects that are not themselves GUI objects, but that contain the functional behavior relevant to a particular GUI object. These smart objects can be easily unit tested like any other class. Once a smart object is in place, a thin GUI view object called a humble dialog can be created that knows how to display the smart object's information, but that contains no application logic or complex behavior. As much as possible, the humble dialog contains only GUI objects with standard behavior, as well as get/set methods that simply read or write values for the GUI elements to display. Testing the GUI behavior then becomes largely optional. If it is tested, these tests don't need to address the functional behavior contained in the smart object.

With the humble dialog approach, the design of GUI object code tends to resemble that shown in Figure 5-1.

Figure 5-1. The smart object AddBook and its view class AddBookView
figs/utf_0501.gif

These classes will implement an "Add Book" dialog for creating a Book . It will be a simple dialog containing a title field, an author field, and Add and Cancel buttons, as shown in Figure 5-2. The smart object AddBook is where all the functional behavior is located. For each behavior, there is a methodin this case, add( ) . The view class AddBookView is a thin object containing the actual GUI objects, such as the window frame, text fields, and buttons. The only custom code it contains is methods to get the field values. Thus, testing the smart object's functionality does not require creating and interacting with the GUI, and testing the view involves only tests of GUI behavior, not application logic.

Figure 5-2. GUI design sketch for the AddBook dialog window
figs/utf_0502.gif

Splitting GUI applications into a presentation layer and a business layer that contains all of the logic is an approach that has long been popular. It makes perfect sense for client-server and web-based applications, where the data and the view are on separate machines. The document-view model follows this philosophy as well, in which all the data lives in a document object and all the presentation- related code is contained in view objects. The humble dialog approach differs from these older architectural ideas by emphasizing that all the important functionality must reside in the smart object. The view object should not contain any functional state, data validation, or other nontrivial logic. This way, all of the functionality can be validated without having to perform complex and fragile GUI testing.