36.

About This Bug Pattern

Good debugging starts with good testing. And with the vast number of invariants that must be checked in GUI code, automated testing is essential. But beware of test suites that only test the state of an underlying model without testing the view as well. When a view incorrectly displays a valid state of the model, you've got yourself a case of the Liar View.

The Symptoms

Most GUI program tests, like program tests in general, follow this structure:

  1. Start the program.

  2. Check some aspect of its state.

  3. Attempt to modify the state.

  4. Check that the state was modified as intended.

Sometimes a manual inspection of the runtime program behavior will contradict the successful results of the tests. Queues may appear on the screen containing elements that testing (supposedly) confirmed were deleted. Objects may contain stale data that was reportedly updated.

Bugs like these may cause us to question our sanity or, worse yet, fall into an unhealthy skepticism of the validity of reason itself. Don't let this happen to you. When used as directed, reason really does work. And, despite reports to the contrary, it is the rare programmer who permanently loses his sanity while coding (permanently being the operative word).

Caution 

At least a portion of this bug may be in the test suite itself.

The Cause

A key to finding these bugs is to realize that at least part of the bug may be in the test suite.

The most common place for a bug to occur in a test on a GUI program is in the last step—checking that the state of the program was modified as intended. This is because GUIs are generally designed with a Model-View-Controller (MVC) architecture. The Swing class library builds this architecture into the structure of the GUI classes.

In an MVC architecture, the internal state of the program is kept in the model. The view responds to events signifying a new state of the model and updates the screen image accordingly. The controller connects these two components together.

The advantage of this architecture is that it decouples the view from the model, so that the implementation of either can change independently. But it poses a challenge to automatic testing methods: it can be difficult to verify that a state change in the model is reflected appropriately in the view. When there is a discrepancy between the two, we have an instance of the Liar View bug pattern.

Note 

It can be difficult to verify that a state change in the model is reflected appropriately in the view.

For example, consider the following simple GUI. It displays the contents of a list of elements as it is updated. Note: Because this GUI is so simple, the architecture is much more straightforward than an industrial-strength MVC architecture. For instance, communication between the model and the view occurs only in one direction (from the model to the view), so the controller does not have to install callbacks (from the view to the model). Nevertheless, the example is adequate for our present purposes.

The main method of the Controller class is used as a simple test. In a real application, I'd move this method into a separate test class and hook it into JUnit.

I've added a pause() method and a PAUSE field to allow us to put the test into slow motion and manually inspect each event as it occurs.

Listing 12-1: The Pause Lets Us Place the Test in Slow-Motion Mode

start example
 import java.awt.*; import java.awt.event.*; import java.util.Vector; import javax.swing.*; public class Controller {   private static final int PAUSE = 1;   private static void assert(boolean assertion) {     if (! assertion) {       throw new RuntimeException("Assertion Failed");     }   }   private void pause() {     try {       synchronized (this) {         wait(PAUSE);       }     }     catch (InterruptedException e) {     }   }   public static void main(String[] args) {     Controller controller = new Controller();     JFrame frame = new JFrame("Test");     Model model = new Model();     JList view = new JList(model);     view.setPreferredSize(new Dimension(200,100));     frame.getContentPane().add(view);     frame.pack();     frame.setVisible(true);     assert(model.getSize() == 0);     controller.pause();     model.add("test0");     controller.pause();     model.add("test1");     controller.pause();     assert(model.getSize() == 2);     controller.pause();     model.remove(0);     controller.pause();     assert(model.getSize() == 1);     controller.pause();     System.exit(0);   } } class Model extends AbstractListModel {   private Vector elements = new Vector();   public synchronized Object getElementAt(int index) {     return elements.get(index);   }   public synchronized int getSize() {     return elements.size();   }   public synchronized void add(Object o) {     int index = this.getSize();     this.elements.add(o);     this.fireIntervalAdded(this, index, index);   }   public synchronized void remove(int index) {     this.elements.remove(index);   } } 
end example

You may have noticed that there is a serious bug in this code. If we run the test case, all assertions succeed, indicating that items are added and removed from the list appropriately. But if we slow things down by, say, setting PAUSE to 1000, we can inspect the test run manually. And guess what? We notice that no items are ever removed in the view.

The reason that the view is not updated is that the remove() method in class Model never calls fireIntervalRemoved() to notify any listeners that the state of the model has changed.

But all the assertions in our test method succeed. Why? Because these assertions check for changes in the model, not the view. Because the model is updated appropriately, the missing event firing is not detected by the assertions.

Note 

Perpetual refactoring is only practical when there is a strong network of unit tests in place, to prevent perpetual code breakage along with it.

Cures and Preventions

There are two ways to fix or prevent the Liar View:

  • Check the model through the view.

  • Automate the physical manipulation of the GUI with the mouse and keyboard.

Checking the Model Through the View

One way to prevent this bug pattern is to check properties of the model as they are reflected in the view. Although this technique limits the properties we can check to those provided in the view, the assertions will at least reflect what's really happening on screen.

For example, we could rewrite Controller.main as follows:

Listing 12-2: Rewriting to Check View Properties After Model State Changes

start example
 import java.awt.*; import java.awt.event*; import java.util.Vector; import java.swing.*; public class Controller {     ...   public static void main(String[] args) {     Controller controller = new Controller();     JFrame frame = new JFrame("Test");     Model model = new Model();     JList view = new JList(model);     view.setPreferredSize(new Dimension(200,100));     frame.getContentPane().add(view);     frame.pack();     frame.setVisible(true);     assert (model.getSize() == 0);     controller.pause();     boolean toggle = model.toggle;     model.add("test0");     assert (toggle == ! model.toggle);     controller.pause();     toggle = model.toggle;     model.add("test1");     assert (toggle == ! model.toggle);     controller.pause();     assert(model.getSize() == 2);     view.setSelectedIndex(0);     assert(view.getSelectedValue().equals("test0"));     controller.pause();     toggle = model.toggle;     model.remove(0);     assert(toggle == ! model.toggle);     controller.pause();     assert(model.getSize() == 1);     view.setSelectedIndex(0);     assert(view.getSelectedValue().equals("test1"));     controller.pause();     System.exit(0);   } }   class Model extends AbstractListModel {     boolean switch = false;     private Vector elements = new Vector();     ...     public void fireIntervalAdded(AbstractListModel m, int start, int end) {       super.fireIntervalAdded(m,start,end);       this.switch = ! this.switch;     }     public void fireIntervalRemoved(AbstractListModel m, int start, int end) {       super.fireIntervalAdded(m,start,end);       this.switch = ! this.switch;     } } 
end example

By using setSelectedIndex() and getSelectedIndex(), we perform a slightly different but much improved test on the program. Not only does the modified test check the model through the view, it also checks the content of selected rows, rather than just the number of rows.

start sidebar
Use Recorders to Test Separately

Testing the model through the view is the best way to prevent Liar Views, as it tests model and view as a combined entity. But it is also advisable to test each component in isolation. By also testing the model (as well as the view) in isolation, we will be able to diagnose bugs introduced into each component much more quickly.

A good strategy for testing a model in isolation is to wire it up to a listener that simulates a view. This simulated view can record the calls made on it, and this record can be checked by the unit tests. Similarly, views can be tested in isolation by wiring them up to simulated models. I call these simulation classes "Recorders," as their sole purpose is to record the sequence of calls performed on them. In cases where we want to check only that a single call was made, a Recorder may simply throw an exception on each call. These exceptions should be caught by the unit tests.

end sidebar

Automate a GUI's Physical Manipulation

Another way to check the view directly is to use the Java Robot class (introduced in Java 1.3) to automate the physical manipulation of a GUI with the mouse and keyboard.

The Robot class lets you take snapshots of subsections of the screen, allowing you to build tests based on the actual physical layout of a GUI view. Of course, this ability can be a disadvantage if the physical layout is not as stable as the logical structure of the view. It can be painful to have to rewrite several tests every time the physical layout changes.

Note 

The Robot class lets you take snapshots of subsections of the screen, allowing you to build tests based on the actual physical layout of a GUI view.

Therefore, I recommend using the Robot class only as a testing tool for a mature GUI whose view won't change very often. To test the logical aspects, call methods on the view as we did in the earlier examples.

By the way, some network installations of Java disable the Robot class functionality out of security concerns.

Avoid These Methods

Beware of methods in view objects that simply trampoline calls back to the model. Doing so can quickly introduce Liar Views. JTables, in particular, contain many such methods.

start sidebar
A Peek at a Real-World Example

Here is one example of a real-world Liar View that occurred in the DrJava project at Rice University.

Shortly after we set DrJava to report the cursor's current line and column numbers in the status bar, we realized that the line and column numbers reported were often wrong. Although a solid suite of unit tests over the GlobalModel ensured that the internal representation of the position was correct, the view wasn't always displaying it properly.

In particular, when moving the cursor with the arrow keys, the line reporting was consistently reporting the position before the last line change. Also, the current column was reported as 1 at every key movement after the first, even though test cases on the model side showed that the current column was computed properly.

Although we were trying to maintain the invariant that all listeners were notified of events through the GlobalModel, new programmers on the project who weren't yet completely familiar with the code base registered the listener for updating the position display directly with a subcomponent of GlobalModel called DefinitionsPane.

Once we realized this, we hypothesized that the problem was a race condition: DefinitionsPane was notifying this listener of a cursor movement before other subcomponents of the GlobalModel were updated. The listener would then poll other components in the GlobalModel to determine the new cursor location, but that new location could be in an inconsistent state. If this hypothesis were correct (it turned out that it was), it explained the lag in the update of line positions, as well as the strange column positions reported by the view.

Jim Van Fleet, one of our developers on the DrJava project, investigated this bug and discovered that it was indeed a race condition: When line changes occurred, a listener we had installed for highlighting text in the view was called by the GlobalModel after the listener for updating the line and column numbers was called by the DefinitionsPane. But calculating what text to highlight involved temporary modification of the GlobalModel position. This temporary modification was not reported to listeners registered directly with the GlobalModel, but our errant listener was able to notice it by polling directly.

In order to release a corrected version of the application as quickly as possible, we implemented a simple fix: we simply synchronized the polling by the errant listener with the calculation of other listeners on the GlobalModel.

In the long term, we've planned a refactoring task to make the errant listener into a GlobalModel listener. That way, there will be no need to synchronize listeners on the various subcomponents (after all, that's the whole point of the GlobalModel).

This bug case study is a good example of how even a moderately large project, with new developers coming on board continually, will need perpetual refactoring in order to maintain the intended invariants. And perpetual refactoring is only practical when there is a strong network of unit tests in place, to prevent perpetual code breakage along with it.

For more on the DrJava project, see the Resources chapter, the section entitled "The GlobalModel Interface" in Chapter 5 entitled "Small Anomalies, Big Problems."

end sidebar



Bug Patterns in Java
Bug Patterns In Java
ISBN: 1590590619
EAN: 2147483647
Year: N/A
Pages: 95
Authors: Eric Allen

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