JaVa
   

The Direct Way

The first idea that a deeply rooted test-first enthusiast will naturally follow is the direct way: why not develop a GUI exactly like all other classes, namely in small steps and with corresponding test cases before writing the app code? Many test-first critics are doubtful about this—a good reason to prove the opposite in a detailed example. We will use the development of a simple user interface for a product catalog as our case study. The product catalog itself can be addressed over the following interface:

public interface ProductCatalog {
 void addProduct(Product product);
 void removeProduct(Product product);
 Set getProducts();
 Set getAvailableCategories();
}


This interface is slim and allows us to add and remove a product and to output all products and all permitted product categories. This means that, to develop the GUI, we don't need a fully functioning implementation, including persistence and other trinkets. In fact, the simplest conceivable implementation will be sufficient, and with SimpleProductCatalog, we have it already (in the codebase on the Web site). But there is still a dependence upon the Product class and the product Category class:

public class Product {
 public Product(String pid) {...}
 public String getPID() {...}
 public Category getCategory() {...}
 public void setCategory(Category category) {...}
 public String getDescription() {...}
 public void setDescription(String description) {...}
}
public class Category {
 public Category(String name) {...}
 public String getName() {...}
}


Encapsulating the entire specialist logic in a separate domain model— ideally in an interface—represents important heuristics: the GUI classes should contain as little logic as possible. This means that the correct coupling between user interface and specialist logic is the only functionality that remains to be tested. In addition, encapsulation of the logic in interfaces rather than in classes contributes to independence and facilitates testing. Thus, improved testability is an additional argument in favor of using the MVC pattern (see also , ). Screenshot shows the design of our graphical user interface for adding and deleting products. Our goal is to create a product editor (Product-Editor) and to ensure the following things:

Java ScreenShot
Screenshot: The desired Product Editor layout.

The exact pixel representation of the interface is not to be tested. Although this would be possible, experience has shown that the layout of an interface is subject to frequent changes. For this reason, the test cases targeted to details would have to be modified often, such that it wouldn't be worth automating them. A visual inspection of the interface within the acceptance tests is better suited to this task. But let's get down to work now. In the first step, we want to test that the editor is properly created and displayed:

public class CatalogEditorTest extends TestCase {
 public void testCreation() {
 CatalogEditor editor = new CatalogEditor();
 editor.show();
 assertTrue(editor.isShowing());
 assertEquals("Product Editor", editor.getTitle());
 }
}


Our implementation is clearly shorter than the test code:

import javax.swing.*;
public class CatalogEditor extends JFrame {
 public CatalogEditor() {
 super("Product Editor");
 }
}


One reviewer commented here that the lines,

editor.show();
assertTrue(editor.isShowing());


check for nothing else but correct functioning of the class javax.swing.JFrame. This is correct, but these lines serve a purpose: they enable us to derive the editor from JFrame. Someone with no idea about Swing would not (be able to) opt for this solution. This shows once more that a known goal, namely to create a Swing app, influences both the design and the implementation. When running the first test, we notice a small flaw: the editor window remains open after the test. So let's add a dispose() to the test and make sure that it's called even when the test fails:

 public void testCreation() {
 try {
 CatalogEditor editor = new CatalogEditor();
 editor.show();
 assertTrue(editor.isShowing());
 assertEquals("Product Editor", editor.getTitle());
 } finally {
 editor.dispose();
 }
 }


Next, we will deal with testing the product list, including the known refactoring in setUp() and tearDown():

public class CatalogEditorTest extends TestCase {
 ...
 private CatalogEditor editor;
 protected void setUp() {
 editor = new CatalogEditor();
 editor.show();
 }
 protected void tearDown() {
 editor.dispose();
 }
 public void testCreation() {
 assertTrue(editor.isShowing());
 assertEquals("Product Editor", editor.getTitle());
 }
 public void testProductList() {
 assertTrue(editor.productList.isShowing());
 }
}


To obtain access to the list widget productList within the test, we have to make it available in an instance variable, either protected or package scope. As an alternative, we could search the component tree for the matching widget. We will see this approach later when testing with JFC-Unit (see ). The implementation is straightforward, totally ignoring the esthetics of the visible layout:

public class CatalogEditor extends JFrame {
 JList productList = new JList();
 public CatalogEditor() {
 super("Product Editor");
 getContentPane().add(productList);
 }
}


Now let's test whether the product list really fills with products from the catalog. We need to make a design decision for this purpose: a Product-Catalog instance is passed to the editor within the constructor. The simple SimpleProductCatalog will do for testing here:

public class CatalogEditorTest extends TestCase {
 ...
 private ProductCatalog catalog;
 protected void setUp() {
 catalog = new SimpleProductCatalog();
 catalog.addProduct(new Product("123456"));
 catalog.addProduct(new Product("654321"));
 editor = new CatalogEditor(catalog);
 editor.show();
 }
 public void testProductList() {
 assertTrue(editor.productList.isShowing());
 ListModel model =
 (ListModel) editor.productList.getModel();
 assertEquals(2, model.getSize());
 }
}


Testing for getSize() alone is relatively weak. We could think of a helper method, which compares the content of a ListModel with the content of a Collection. For the moment, however, we are brave and contented with simple things, leading to the following implementation:

public class CatalogEditor extends JFrame {
 JList productList;
 public CatalogEditor(ProductCatalog catalog) {
 super("Product Editor");
 productList =
 new JList(catalog.getProducts().toArray());
 getContentPane().add(productList);
 }
}


This test tells us nothing about how the single products should be represented. And the implementation relies entirely on the toString() method in Product. By the way, this is a general weakness of the approach described here: we are merely testing the model of the graphical components and not what is really displayed. The reason is that the Swing widgets (i.e., the JList class in this case) don't let us access the actual representation. Let's see how products are deleted, the Delete button test is easy:

public class CatalogEditorTest extends TestCase {
 ...
 public void testDeleteButton() {
 assertTrue(editor.deleteButton.isShowing());
 assertEquals("Delete", editor.deleteButton.getText());
 }
}


and so is the implementation:

public class CatalogEditor extends JFrame {
 ...
 JButton deleteButton;
 public CatalogEditor(ProductCatalog catalog) {
 super("Product Editor");
 productList =
 new JList(catalog.getProducts().toArray());
 getContentPane().add(productList);
 deleteButton = new JButton("Delete");
 getContentPane().add(deleteButton);
 }
}



For this reason, we want to proceed to the actual Delete function, or actually to the corresponding test:

public class CatalogEditorTest extends TestCase {
 ...
 private ListModel getListModel() {
 return (ListModel) editor.productList.getModel();
 }
 public void testDeleteProduct() {
 editor.productList.setSelectedIndex(0);
 editor.deleteButton.doClick();
 assertEquals(1, getListModel().getSize());
 assertEquals(new Product("654321"),
 getListModel().getElementAt(0));
 assertEquals(1, catalog.getProducts().size());
 assertTrue(catalog.getProducts().contains(
 new Product("654321")));
 }
}


Note that both the ListModel and the catalog itself have to be tested, where the number of products alone is not sufficient this time. The test in this form only makes sense provided that Product.equals() has been previously implemented, namely, when the equality of two products is determined by their product IDs. We quickly dare a first implementation attempt:

public class CatalogEditor extends JFrame
 implements ActionListener {
 ...
 private ProductCatalog catalog;
 public CatalogEditor(ProductCatalog catalog) {
 super("Product Editor");
 this.catalog = catalog;
 productList =
 new JList(catalog.getProducts().toArray());
 getContentPane().add(productList);
 deleteButton = new JButton("Delete");
 deleteButton.addActionListener(this);
 getContentPane().add(deleteButton);
 }
 public void actionPerformed(ActionEvent e) {
 deleteButtonClicked();
 }
 private void deleteButtonClicked() {
 Product toDelete =
 (Product) productList.getSelectedValue();
 catalog.removeProduct(toDelete);
 productList.setListData(
 catalog.getProducts().toArray());
 }
}


This solution gets by without separate implementation of the ListModel interface. But the experienced Swing programmer's hair stands on end; he or she wants a "neat and tidy" ListModel. We want that too, but because the code will communicate better, rather than because it will make the code shorter. For this reason, we will turn our attention away from the editor and concentrate on a ProductsListModel instead. And because we are right in the swing of it, [1] let's take bigger steps. Our first test focuses on creating things:

public class ProductsListModelTest extends TestCase {
 public void testCreation() {
 ProductCatalog catalog = new SimpleProductCatalog();
 catalog.addProduct(new Product("123456"));
 catalog.addProduct(new Product("654321"));
 ProductsListModel model =new ProductsListModel(catalog);
 assertEquals(2, model.getSize());
 assertEquals(new Product("123456"), model.getElementAt(0));
 assertEquals(new Product("654321"), model.getElementAt(1));
 }
}



Considering that the test requires lexicographic sorting of the products, while the catalog supplies an (unordered) Set, we realize that everything is harder than expected:

public class ProductsListModel extends AbstractListModel {
 private ProductCatalog catalog;
 private List sortedProducts;
 public ProductsListModel(ProductCatalog catalog) {
 this.catalog = catalog;
 sortedProducts = new ArrayList(catalog.getProducts());
 Collections.sort(sortedProducts, new Comparator() {
 public int compare(Object o1, Object o2) {
 Product p1 = (Product) o1;
 Product p2 = (Product) o2;
 return p1.getPID().compareTo(p2.getPID());
 }
 });
 }
 public Object getElementAt(int index) {
 return sortedProducts.get(index);
 }
 public int getSize() {
 return sortedProducts.size();
 }
}


Our next testing step is targeted to delete products from ListModel:

public class ProductsListModelTest extends TestCase {
 ...
 private ProductCatalog catalog;
 private ProductsListModel model;
 protected void setUp() {
 catalog = new SimpleProductCatalog();
 catalog.addProduct(new Product("123456"));
 catalog.addProduct(new Product("654321"));
 model = new ProductsListModel(catalog);
 }
 public void testDeleteProduct() {
 model.deleteProduct(0);
 assertEquals(1, model.getSize());
 assertEquals(new Product("654321"),model.getElementAt(0));
 assertEquals(1, catalog.getProducts().size());
 assertTrue(catalog.getProducts().contains(
 new Product("654321")));
 }
}


It is no coincidence that this test case is very similar to the delete test in CatalogEditorTest. It should occasionally cause us to consider whether or not the old test has become pointless by the new one. The interface decision was taken in favor of delete(int index), because we can easily determine the selected index from JList. This makes the implementation pretty easy:

public class ProductsListModel extends AbstractListModel {
 ...
 public void deleteProduct(int index) {
 Product toDelete = (Product) sortedProducts.get(index);
 sortedProducts.remove(toDelete);
 catalog.removeProduct(toDelete);
 }
}


And now back to the editor to embed the ProductListModel:

public class CatalogEditor extends JFrame implements ActionListener {
 ...
 public CatalogEditor(ProductCatalog catalog) {
 super("Product Editor");
 ProductsListModel model =
 new ProductsListModel(catalog);
 productList = new JList(model);
 ...
 }
 public void actionPerformed(ActionEvent e) {
 deleteButtonClicked();
 }
 private void deleteButtonClicked() {
 int deleteIndex = productList.getSelectedIndex();
 ((ProductsListModel) productList.
 getModel()).deleteProduct(deleteIndex);
 }
}


Isn't this wonderful? All tests are running! It's about time we take a look at all of this "for real." All we need first is a main() method and a rudimentary layout:

public class CatalogEditor extends JFrame implements ActionListener {
 ...
 public CatalogEditor(ProductCatalog catalog) {
 super("Product Editor");
 createWidgets(catalog);
 }
 private void createWidgets(ProductCatalog catalog) {
 getContentPane().setLayout(new FlowLayout());
 setSize(150, 150);
 ProductsListModel model = new ProductsListModel(catalog);
 productList = new JList(model);
 getContentPane().add(productList);
 deleteButton = new JButton("Delete");
 deleteButton.addActionListener(this);
 getContentPane().add(deleteButton);
 }
 public static void main(String[] args) {
 SimpleProductCatalog catalog = new SimpleProductCatalog();
 catalog.addProduct(new Product("1000001"));
 catalog.addProduct(new Product("2000002"));
 CatalogEditor editor = new CatalogEditor(catalog);
 editor.show();
 }
}


Unfortunately, visual testing has an unpleasant surprise in store for us: Although all tests are running, clicking the Delete button once does not make the selected product disappear! How could this happen? The answer is that we were taken in by the Liar View bug pattern [Allen01]: although the model behind JList is updated, the view itself is not informed about it. The correct thing to do would be to inform all ListDataListeners registered with ProductsListModel about a successful deletion. So let's extend a corresponding test case:

import javax.swing.event.ListDataEvent;
public class ProductsListModelTest extends TestCase {
 ...
 private boolean intervalRemovedCalled = false;
 public void testDeleteProduct() {
 ListDataListener listener = new ListDataListener() {
 public void intervalAdded(ListDataEvent e) {}
 public void intervalRemoved(ListDataEvent e) {
 intervalRemovedCalled = true;
 }
 public void contentsChanged(ListDataEvent e) {}
 };
 model.addListDataListener(listener);
 model.deleteProduct(0);
 assertTrue(intervalRemovedCalled);
 assertEquals(1, model.getSize());
 assertEquals(new Product("654321"),model.getElementAt(0));
 assertEquals(1, catalog.getProducts().size());
 assertTrue(catalog.getProducts().contains(
 new Product("654321")));
 }
}



Similar to examples in other chapters, we are using a poor-house mock object, that is, an anonymous class. The implementation needs one small addition:

public class ProductsListModel extends AbstractListModel {
 ...
 public void deleteProduct(int index) {
 Product toDelete =
 (Product) sortedProducts.get(index);
 sortedProducts.remove(toDelete);
 catalog.removeProduct(toDelete);
 fireIntervalRemoved(this, index, index);
 }
}


Now, the visual test works as expected. And just to make sure nobody can say we are careless, let's see a few functionality chunks in express testing. Chunk 1: Test to ensure that the Delete button is enabled only when a product is selected:

 public void testDeleteButton() {
 assertTrue(editor.deleteButton.isShowing());
 assertEquals("Delete", editor.deleteButton.getText());
 assertFalse(editor.deleteButton.isEnabled());
 editor.productList.setSelectedIndex(0);
 assertTrue(editor.deleteButton.isEnabled());
 editor.productList.clearSelection();
 assertFalse(editor.deleteButton.isEnabled());
 }


Chunk 2: Test to ensure that the product detail view works correctly:

 public void testProductDetails() throws Exception {
 assertTrue(editor.productDetails.isShowing());
 assertEquals("", editor.productDetails.getText());
 Product product =
 (Product) getListModel().getElementAt(0);
 product.setCategory(new Category("Records"));
 product.setDescription("The best of Kenny Haye");
 editor.productList.setSelectedIndex(0);
 assertProductDetails(product);
 editor.productList.setSelectedIndex(1);
 assertProductDetails(
 (Product) getListModel().getElementAt(1));
 editor.productList.clearSelection();
 assertEquals("", editor.productDetails.getText());
 editor.productList.setSelectedIndex(0);
 assertProductDetails(product);
 }
 private void assertProductDetails(Product product)
 throws IOException {
 BufferedReader reader = new BufferedReader(
 new StringReader(editor.productDetails.getText()));
 assertEquals("PID: " + product.getPID(),
 reader.readLine());
 assertEquals("Category: " +
 product.getCategory().getName(),
 reader.readLine());
 assertEquals(product.getDescription(),
 reader.readLine());
 assertNull(reader.readLine());
 }


This test case already considers deselection of a product and sequential selection of multiple products. The test in this form hasn't been built in one go, but piece by piece. It also shows that visually controlling the components from time to time can be useful to find new test cases. Let's have another look at the current implementation state of CatalogEditor:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class CatalogEditor extends JFrame
 implements ActionListener, ListSelectionListener {
 JList productList;
 JButton deleteButton;
 JTextArea productDetails;
 public CatalogEditor(ProductCatalog catalog) {
 super("Product Editor");
 createWidgets(catalog);
 }
 private void createWidgets(ProductCatalog catalog) {
 getContentPane().setLayout(new FlowLayout());
 setSize(150, 150);
 addProductList(catalog);
 addProductDetails();
 addDeleteButton();
 }
 private void addProductList(ProductCatalog catalog) {
 ProductsListModel model =
 new ProductsListModel(catalog);
 productList = new JList(model);
 productList.setSelectionMode(
 ListSelectionModel.SINGLE_SELECTION);
 productList.addListSelectionListener(this);
 getContentPane().add(productList);
 }
 private void addProductDetails() {
 productDetails = new JTextArea();
 getContentPane().add(productDetails);
 }
 private void addDeleteButton() {
 deleteButton = new JButton("Delete");
 deleteButton.setEnabled(false);
 deleteButton.addActionListener(this);
 getContentPane().add(deleteButton);
 }
 public void actionPerformed(ActionEvent e) {
 deleteButtonClicked();
 }
 private void deleteButtonClicked() {
 int deleteIndex = productList.getSelectedIndex();
 ((ProductsListModel) productList.getModel()).
 deleteProduct(deleteIndex);
 }
 public void valueChanged(ListSelectionEvent e) {
 if (productList.getSelectedValue() == null) {
 clearProductSelection();
 } else {
 selectProduct(
 (Product) productList.getSelectedValue());
 }
 }
 private void clearProductSelection() {
 deleteButton.setEnabled(false);
 productDetails.setText("");
 }
 private void selectProduct(Product product) {
 deleteButton.setEnabled(true);
 productDetails.setText("");
 productDetails.append("PID: " +
 product.getPID() + "\n");
 productDetails.append("Category: " +
 product.getCategory().getName() + "\n");
 productDetails.append(
 product.getDescription() + "\n");
 }
}


We find nothing that could be a big surprise for Swing programmers, perhaps with the only exception of gradually losing patience as the layout still leaves a great deal to be desired. However, one important piece of functionality is still missing: the Add button. First, the button itself in a trivial test:

 public void testAddButton() {
 assertTrue(editor.addButton.isShowing());
 assertEquals("Add", editor.addButton.getText());
 }



And now let's see what's behind it. The problem we have to deal with here is that another dialog should open for the user to enter new product data. But how can we test such a thing? Here is our answer: not at all in the test suite for CatalogEditor! In this case, we are only interested in having the editor somehow get a hold of a new product instance:

 public void testAddProduct() {
 editor.setProductCreator(new ProductCreator() {
 public Product create() {
 return new Product("333333");
 }
 });
 editor.addButton.doClick();
 assertEquals(3, getListModel().getSize());
 assertEquals(new Product("333333"),
 getListModel().getElementAt(1));
 assertEquals(3, catalog.getProducts().size());
 assertTrue(catalog.getProducts().contains(
 new Product("333333")));
 }


This approach deserves a closer look. We have decided to move the creation of a new product to a ProductCreator interface. It allows us to use a dummy implementation for testing purposes. Another trick is the PID we have actually chosen, which means that we concurrently test for correct sorting of the list. To keep it happy, the test requires a considerable amount of code:

public class CatalogEditor extends JFrame
 implements ActionListener, ListSelectionListener {
 ...
 JButton addButton;
 private ProductCreator productCreator;
 private void createWidgets(ProductCatalog catalog) {
 ...
 addAddButton();
 }
 public void actionPerformed(ActionEvent e) {
 if (e.getSource() == deleteButton) {
 deleteButtonClicked();
 } else {
 addButtonClicked();
 }
 }
 private ProductsListModel getProductsListModel() {
 return ((ProductsListModel) productList.getModel());
 }
 private void addAddButton() {
 addButton = new JButton("Add");
 addButton.addActionListener(this);
 getContentPane().add(addButton);
 }
 private void addButtonClicked() {
 Product newProduct = productCreator.create();
 getProductsListModel().addProduct(newProduct);
 }
 protected void setProductCreator(
 ProductCreator creator) {
 productCreator = creator;
 }
}
public class ProductsListModel extends AbstractListModel {
 ...
 private void sortProducts() {
 Collections.sort(sortedProducts, new Comparator() {
 public int compare(Object o1, Object o2) {
 Product p1 = (Product) o1;
 Product p2 = (Product) o2;
 return p1.getPID().compareTo(p2.getPID());
 }
 });
 }
 public void addProduct(Product newProduct) {
 sortedProducts.add(newProduct);
 sortProducts();
 catalog.addProduct(newProduct);
 }
}



We can see that two classes had to be modified. The test chunk was actually too big for one single iteration. But everything seems to have gone well. Nope! Actually, the Liar View bug pattern we saw before sneaked in again: a corresponding test is still missing in ProductsListModelTest. To make sure our readers won't sink into a testing monotony, we will turn the tables this time—we will give only the bug fix and leave it up to the readers to write a matching test as an exercise:

 public void addProduct(Product newProduct) {
 sortedProducts.add(newProduct);
 sortProducts();
 catalog.addProduct(newProduct);
 int index = sortedProducts.indexOf(newProduct);
 fireIntervalAdded(this, index, index);
 }


Let's stop the development of this graphical product catalog editor at this point. [2] The following remains to be done or tested:

These requirements do not produce new test-first problems, so we can confidently leave them up to the experienced reader. Besides, nobody has to be ashamed of using a "visual" test to actually discover additional test cases or errors and to then translate them back into JUnit test cases. To work with an example based on this approach, we recommend our readers delete a product and then look at the buttons and product details. You will see that several things are wrong!

Brief Summary

As with our test-first development of servlets (see , ), we have gotten to a point where the full functionality of the graphical editor is not yet available on a functional level, because a certain component (i.e., ProductCreatorDialog in this case) is still missing. But still, we can test the behavior depending on this in unit tests. However, the layout of the product catalog editor is still unsatisfactory. The current layout is anything but pretty (Screenshot). Theoretically, we could check everything by automated testing. For example, [URL:WakeGUI] also tests for the relative position of widgets to each other. However, such test cases are often very unstable, because the layout of a GUI normally changes more often than its functionality. For this reason, we should carefully consider whether or not this type of test automation would better be implemented as part of the functional test suite. On the one hand, this would mean using different testing tools; on the other hand, we would get direct feedback from the users.

Java Click To expand
Screenshot: A "rudimentary" layout.

We will content ourselves with a (slightly) improved and refactored implementation of the layout at this point. This improved version takes us a step closer to the desired layout, as shown in Screenshot:

Java ScreenShot
Screenshot: The improved layout.
public class CatalogEditor extends JFrame
 implements ActionListener, ListSelectionListener {
 ...
 private void createWidgets(ProductCatalog catalog) {
 buildProductDetails();
 buildProductList(catalog);
 buildAddButton();
 buildDeleteButton();
 buildLayout();
 }
 private void buildLayout() {
 getContentPane().setLayout(new BorderLayout(5,5));
 setSize(300, 300);
 JPanel buttonPane = new JPanel();
 buttonPane.setLayout(new GridLayout(2,1));
 getContentPane().add(new JScrollPane(productDetails),
 BorderLayout.SOUTH);
 getContentPane().add(new JScrollPane(productList),
 BorderLayout.CENTER);
 buttonPane.add(addButton);
 buttonPane.add(deleteButton);
 getContentPane().add(buttonPane, BorderLayout.EAST);
 }
 private void buildProductDetails() {
 productDetails = new JTextArea();
 productDetails.setPreferredSize(
 new Dimension(50,100));
 }
 private void buildProductList(ProductCatalog catalog) {
 ProductsListModel model =
 new ProductsListModel(catalog);
 productList = new JList(model);
 productList.setSelectionMode(
 ListSelectionModel.SINGLE_SELECTION);
 productList.addListSelectionListener(this);
 }
 private void buildDeleteButton() {
 deleteButton = new JButton("Delete");
 deleteButton.setEnabled(false);
 deleteButton.addActionListener(this);
 }
 private void buildAddButton() {
 addButton = new JButton("Add");
 addButton.addActionListener(this);
 }
}



Keeping the GUI Clear

The basic principle to facilitate GUI testing or to even make it almost superfluous is to keep all logic out of the GUI classes. In the preceding example we still had two kinds of logic within those classes:

If we could eliminate the first point by extracting the interdependency logic into a class of its own, we could argue that the actual wiring is mere delegation, which is not really worthwhile testing. This is exactly the approach that Michael Feathers [02b] describes in The Humble Dialog Box. He presents a test-first technique to extract all user interface logic from the GUI class itself into a "smart object," which collaborates with an abstract view and can thus be developed and tested independently. The actual "humble" view implementation does nothing more than provide simple, delegating getter and setter methods. [1]countPuns++ [2]The code available from our Web site includes the complete example.


JaVa
   
Comments