Bug Pattern In Java - The Liar View - Online Article

Overview

GUIs are usually designed with a Model-View-Controller architecture in which the view is decoupled from the model. This separation presents a challenge to automated testing because it's difficult to verify that a state change in the model is reflected appropriately in the view—it spawns the Liar View bug pattern. We discuss how you can make sure both model and view get tested.

Picture this: You've crafted a golden GUI program designed for a distributed system, all that your customer asked for and then some. You've run it through an extensive suite of automated unit tests. It came back with a clean bill of health.

The pressure is on to deliver the GUI, but being the exacting programmer that you are, you fire it up for one last manual test only to discover erroneous behavior—behavior that should have been caught by the automated tests. If only you could have prevented this situation! Well, you can.

Sometimes, despite passing its suite of tests, a program will, upon manual inspection, exhibit erroneous behavior that should have been discovered by one of the tests. Such behavior is common in distributed and multithreaded systems. In these cases, the nondeterministic nature of the program is often the cause.

But in the case of GUIs, there is another common cause: the Liar View bug pattern.

Here's the bug pattern in a nutshell:

  • Pattern: Liar View.
  • Symptoms: A GUI program passes a suite of tests, but then exhibits behavior that should've been ruled out by those tests.
  • Cause: The tests only check aspects of the model directly.
  • Cures and Preventions: Write independent tests for the model and the view plus tests on the combined entity.

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.

The Pause Lets Us Place the Test in Slow-Motion Mode

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);
}
}

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:

Rewriting to Check View Properties After Model State Changes

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;
}
}

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.

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.

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.

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.

GUIs Aren't the Only Liars!

Although Liar Views occur quite often in GUI code, the underlying pattern is actually more general than that. Any time we are displaying data, be it in textual or graphical form, there is the potential for a Liar View.

For example, suppose you are writing a toString() method over a composite data structure and you forget to include the value of one of the fields (or worse, you include another field in its place). If you print these Strings out during, say, debugging, it can send you on a serious wild goose chase. Textual Liar Views can be at least as dangerous as GUI-based Liar Views. Fortunately, they are a lot more straightforward to eliminate through testing.

In fact, toString() test methods are some of the easiest and most fun test methods to write (yes, testing really can be fun). Because String values can be represented literally in Java, test methods on toString() can consist entirely of a series of assertions that check that various calls to toString() match designated String literals.

What We've Learned

In this article on the Liar View bug pattern, we've learned the following:

  • Most GUIs are built on the Model-View-Controller architecture (MVC), which decouples the model from the view.
  • In an MVC architecture, the internal state of the program is kept in the controller connects these two components together.
  • The decoupled aspect of the GUI architecture means that program test results may differ from runtime behavior.
  • A key to diagnosing this bug is to realize that part of the bad behavior can be in the test itself.
  • 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. It can be difficult to verify that a state change in the model is reflected appropriately in the view. Assertions in the test method may succeed even though the view is not updated because the assertions only check for changes in the model.
  • One way to prevent this bug pattern is to check the model through the view.
  • Another way to check the view directly is to use the Java Robot class to automate the physical manipulation of a GUI with the mouse and keyboard. This class also lets you take snapshots of subsections of the screen, allowing you to build tests based on the actual physical layout of a GUI view. However, I recommend using the Robot class only as a testing tool for mature GUIs whose views won't change very often.
  • Use Recorders to test the model (and the view) in isolation.
  • Beware of methods in view objects that trampoline calls back to the model. Doing so can introduce Liar Views. JTables, in particular, contain many such methods.
  • Any time we are displaying data, be it in textual or graphical form, there is the potential for a Liar View.
  • Perpetual refactoring is only practical when there is a strong network of unit tests in place, to prevent perpetual code breakage along with it.

Bit by bit we're working our way through solutions to the most common (and most frustrating) bugs, but we still have a lot of ground to cover.

About the Author:

No further information.




Comments

No comment yet. Be the first to post a comment.