5.1 Library GUI

     

This chapter presents a Java Swing implementation of a GUI for the virtual library. It demonstrates test driven development of a GUI application and serves as a more realistic example of a working library application than the previous chapters. However, it is still a demonstration program rather than a practically useful application.

The functional goal of the library GUI is to allow a user to add new Book s to a Library and look up existing Book s. Nothing fancy, but like most GUI applications, there are many elements and behaviors to consider. The application will need a main window, dialogs for adding and finding Book s, and standard GUI functionality such as being able to open and close windows .

Adhering to the TDD mantra, first identify a behavior and write a test for it. The initial target is the Add Book dialog described earlier, which provides a GUI interface for creating a new Book . Example 5-2 shows the unit test for the smart object AddBook .

Example 5-2. Initial unit test for the smart object AddBook
 AddBookTest.java public class  AddBookTest  extends TestCase {    public void  testAddBook  ( ) {       Library library = new Library( );  AddBook  addBook = new AddBook(library);       addBook.  add  ("The Dragons of Eden", "Carl Sagan");       assertNotNull( library.getBooksByTitle("The Dragons of Eden") );    } } 

This test creates an AddBook object, calls its method add( ) , and verifies that the Book has been added to the Library . AddBook 's constructor gives the test the Library to modify; this was an up-front design decision.

The initial implementation of AddBook to pass AddBookTest is shown in Example 5-3.

Example 5-3. Initial version of the smart object AddBook
 AddBook.java public class  AddBook  {    private Library library;    public  AddBook  (Library lib) {       library = lib;    }    public void  add  (String title, String author) {       Book book = new Book( title, author );       try {          library.  addBook  ( book );       } catch (Exception e) {}    } } 

Since the method addBook() may throw an exception, add() must catch it. Informing the user about the error is something to add to the "to-do" list. Otherwise, the implementation is simple.

The next step is to create the view class, AddBookView . It needs to provide a GUI window, text fields for the title and author, and Add and Cancel buttons . The window title should be "Add Book." The unit test AddBookViewTest verifies all of this, as shown in Example 5-4.

Example 5-4. The unit test AddBookViewTest
 AddBookViewTest.java public class  AddBookViewTest  extends TestCase {    public void  testControlValues  ( ) {  AddBookView  view = new AddBookView( );       assertEquals( "Add Book", view.getTitle( ) );       assertEquals( "", view.  titleField  .getText( ) );       assertEquals( "", view.  authorField  .getText( ) );       assertEquals( "Add", view.  addButton  .getText( ) );       assertEquals( "Cancel", view.  cancelButton  .getText( ) );    } } 

Example 5-5 gives the initial implementation of AddBookView . It is a custom subclass of the Swing GUI class JFrame and contains only the minimum code necessary to pass the test. It completely ignores the layout of the controls.

Example 5-5. The initial version of AddBookView
 AddBookView.java import java.awt.*; import javax.swing.*; public class  AddBookView  extends JFrame {    protected JTextField titleField;    protected JTextField authorField;    protected JButton cancelButton;    protected JButton addButton;    public  AddBookView  ( ) {       super("Add Book");       Container contentPane = this.getContentPane( );       // Add labels and text fields       JLabel label1 = new JLabel("Title", Label.RIGHT);       contentPane.add(label1);       titleField = new JTextField("", 60);       contentPane.add(titleField);       JLabel label2 = new JLabel("Author", Label.RIGHT);       contentPane.add(label2);       authorField = new JTextField("", 60);       contentPane.add(authorField);       // Add buttons       cancelButton = new JButton("Cancel");       contentPane.add(cancelButton);       addButton = new JButton("Add");       contentPane.add(addButton);    } } 

When Java creates a JFrame -derived dialog window, it does not display it until its show( ) method is called. So, the test AddBookViewTest creates, verifies, and destroys the AddBookView dialog without actually showing it.

Now AddBook can be made to use AddBookView . Some thought must be given to how the smart object and the humble dialog will interact. They will communicate via an implicit internal protocol. The ideal architecture will place all the important behavior in the smart object, and will place all the GUI- related code, such as event handling, in the view. Having both the smart object and the view know about each other is unnecessary. The only necessary interaction between the two is that the view needs to be able to call the methods on the smart object representing its behaviors, so we will follow that model. When the view is constructed , it will get a reference to its smart object.

The most important functionality of this construct is to add a Book when the user clicks on the Add button in the view. This is a GUI-driven behavior, so the unit test belongs in AddBookViewTest . It also implicitly tests that the view invokes the smart object's add() method. Example 5-6 shows this test.

Example 5-6. AddBookViewTest with test of the Add button
 AddBookViewTest.java public class  AddBookViewTest  extends TestCase {    private  Library  library;    private  AddBook  addBook;    private  AddBookView  view;    public void setUp( ) {       library = new Library( );       addBook = new AddBook(library);       view = new AddBookView(addBook);    }    public void  testAddButton  ( ) {       view.titleField.setText("The Dragons of Eden");       view.authorField.setText("Carl Sagan");       view.addButton.doClick( );       assertEquals(1,           library.getBooksByTitle("The Dragons of Eden").size( ));    } } 

AddBookViewTest is refactored as a test fixture that creates instances of Library , AddBook , and AddBookView in its setUp( ) method. The new test method, testAddButton( ) , sets the title and author text field values, simulates a user click on the Add button using the method JButton.doClick() , and verifies that the Book is added to the Library .

To pass this test, AddBookView requires a number of additions, including a reference to an AddBook and the ability to handle the button click event. Example 5-7 shows the new version of AddBookView . The code to create and add the controls is moved to a new method, addControls( ) (which is not shown, for brevity).

Example 5-7. AddBookView with "Add" button functionality
 AddBookView.java public class  AddBookView  extends JFrame       implements ActionListener {    protected JTextField titleField;    protected JTextField authorField;    protected JButton cancelButton;    protected JButton addButton;    private AddBook addBook;    public AddBookView(AddBook ab) {       super("Add Book");       addBook = ab;       addControls( );       addButton.addActionListener( this );       cancelButton.addActionListener( this );    }    public void  actionPerformed  (ActionEvent e) {       String cmd = e.getActionCommand( );       System.out.println(cmd);       if ( cmd.equals("Add") ) {          addBook.add(titleField.getText( ), authorField.getText( ));       }       else          System.out.println("cmd not handled: "+cmd);    } } 

The result of the changes to AddBookView is that it receives notification of user events via the actionPerformed() method. If the event indicates that the Add button was clicked, it calls the method AddBook.add() with the title and author values.

Now the Add Book dialog can be tried out manually. Example 5-8 shows a simple executable class called CreateAddBook that creates the dialog.

Example 5-8. Simple executable class to create AddBook dialog
 CreateAddBook.java public class  CreateAddBook  {    public static void  main  (String[] args) {  CreateAddBook  create = new CreateAddBook( );    }    public  CreateAddBook  ( ) {  Library  library = new Library( );  AddBook  addBook = new AddBook(library);  AddBookView  view = new AddBookView(addBook);       view.show( );       while (view.isVisible( )) {}       System.exit(0);    } } 

Assuming that the directory containing the library classes is in the Java CLASSPATH , CreateAddBook is run as follows :

 $ java CreateAddBook 

The Add Book dialog appears as just a titlebar. It can be resized to show that the Add button fills the entire frame, as shown in Figure 5-3.

Figure 5-3. The AddBook dialog as it initially appears
figs/utf_0503.gif

The dialog is effectively useless because nothing is being done in AddBookView to arrange and size the controls. Once a better layout is implemented, the dialog looks much better, as shown in Figure 5-4.

Figure 5-4. The AddBook dialog with improved layout
figs/utf_0504.gif

Example 5-9 shows the method addControls() with the new layout code. Aside from arranging the controls with a GridBagLayout , the method sets the dialog to a usable default size with the setSize( ) method.

Example 5-9. addControls( ) with improved layout code
 AddBookView.java    protected void  addControls( )  {       Container contentPane = this.getContentPane( );       contentPane.setLayout(new  GridBagLayout  ( ));       GridBagConstraints c = new GridBagConstraints( );       // Add labels and text fields       JLabel label1 = new JLabel("Title", Label.RIGHT);       c.insets = new Insets(2, 2, 2, 2);       c.gridx = 0;       c.gridy = 0;       contentPane.add(label1, c);       titleField = new JTextField("", 60);       titleField.setMinimumSize(new Dimension(180, 30));       c.gridx = 1;       contentPane.add(titleField, c);       JLabel label2 = new JLabel("Author", Label.RIGHT);       c.gridx = 0;       c.gridy = 1;       contentPane.add(label2, c);       authorField = new JTextField("", 60);       authorField.setMinimumSize(new Dimension(180, 30));       c.gridx = 1;       contentPane.add(authorField, c);       // Add buttons       cancelButton = new JButton("Cancel");       c.gridx = 0;       c.gridy = 2;       contentPane.add(cancelButton, c);       addButton = new JButton("Add");       c.gridx = 1;       contentPane.add(addButton, c);  setSize  (300, 140);    } 

Why isn't there a unit test for the new layout code? It is overkill to write unit tests for purely visual attributes such as layout positions and control sizes. Not only are such tests tedious to write, but their value is limited. If someone adjusts the position of a control, the code's functional behavior does not change, so why should the unit test fail?

Now a working Add Book dialog is in place, along with unit tests of its functionality. The library GUI needs a lot more to be usable, including a main window, a Find Book dialog, and a lot of related GUI functionality and application logic. Rather than walking through all the steps to build this application, this description skips ahead to the finished result. The final version of the library GUI application contains a number of GUI elements implemented as smart objects with thin view classes, including:

  • AddBook and AddBookView

  • FindBookByTitle and FindBookByTitleView

  • LibraryFrame and LibraryFrameView

The class LibraryFrame is the main application window with an attached menu bar. It acts as the parent for the other windows. Closing it causes the application to exit. It is shown in Figure 5-3Figure 5-3.

Figure 5-5. The LibraryFrame window
figs/utf_0505.gif

Since the view classes have duplicate code and the same interface, it makes sense to create a common base class. The abstract parent class BaseView is a simple subclass of JFrame . Example 5-10 shows the code for BaseView .

Example 5-10. The abstract base class BaseView
 BaseView.java public abstract class  BaseView  extends  JFrame  implements ActionListener {  BaseView  (String title, int width, int height) {       super(title);       addControls( );       setSize(width, height);    }    public abstract void  actionPerformed  (ActionEvent e);    protected abstract void  addControls( )  ; } 

BaseView gives the view classes a consistent interface and eliminates code duplication between them. As an abstract class, BaseView should be tested with an AbstractTest. BaseViewTestCase is shown in Example 5-11.

Example 5-11. The AbstractTest BaseViewTestCase
 BaseViewTestCase.java public abstract class  BaseViewTestCase  extends TestCase {    public abstract BaseView  getBaseView( )  ;    public void  testNotVisible( )  {       BaseView view = getBaseView( );       assertFalse( view.isVisible( ) );    }    public void  testShow( )  {       BaseView view = getBaseView( );       view.show( );       assertTrue( view.isVisible( ) );    }    public void  testClose( )  {       BaseView view = getBaseView( );       view.show( );       WindowEvent e = new WindowEvent(view,          WindowEvent.WINDOW_CLOSING);       Toolkit.getDefaultToolkit( ).getSystemEventQueue( ).postEvent(e);       try {          Thread.currentThread( ).sleep(100);       } catch(Exception x) {}       assertFalse( view.isVisible( ) );    } } 

The AbstractTest tests three behaviors that all classes derived from BaseView should exhibit: they are hidden upon creation, become visible after the show( ) method is called, and are hidden again after a WINDOW_CLOSING event is sent.

The unit tests for the view classes derived from BaseView should be subclasses of BaseViewTestCase . Example 5-12 shows AddBookViewTest implemented this way.

Example 5-12. AddBookViewTest implemented as a subclass of BaseViewTestCase
 AddBookViewTest.java public class  AddBookViewTest  extends  BaseViewTestCase  {    private Library library;    private AddBook addBook;    private AddBookView view;    public BaseView  getBaseView( )  {       return new AddBookView( addBook );    }    public void  setUp( )  {       library = new Library( );       addBook = new AddBook( library );       view = (AddBookView)  getBaseView( )  ;       view.show( );    }    public void  tearDown( )  {       addBook = null;       library = null;    }    public void  testAddButton( )  {       view.titleField.setText("The Dragons of Eden");       view.authorField.setText("Carl Sagan");       view.addButton.doClick( );       assertEquals(1, library.getBooksByTitle("The Dragons of Eden").                    size( ));       assertEquals( "", view.titleField.getText( ) );       assertEquals( "", view.authorField.getText( ) );       assertFalse( view.isVisible( ) );    } } 

Note how the test implements and uses the factory method getBaseView( ) to create an instance of AddBookView for the tests.

In conclusion, although unit tests for a GUI-driven application use different strategies than tests for ordinary classes, the same basic patterns of unit test development are followed. Each class has a corresponding test class, and each behavior is tested with a separate test method.



Unit Test Frameworks
Unit Test Frameworks
ISBN: 0596006896
EAN: 2147483647
Year: 2006
Pages: 146
Authors: Paul Hamill

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net