 | Level: Introductory Eric Allen (eallen@cyc.com), Lead Java Developer, Cycorp, Inc.
23 Apr 2001 GUIs are generally designed with a model-view-controller architecture in which the view is decoupled from the model. The 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 infamous "Liar View." This installment of Diagnosing Java Code examines the Liar View bug pattern. Liar, liar!
Picture this: you've crafted a golden GUI program designed for a distributed system, all that the client asked for and then some. You've run it through an automated test suite -- because the number of invariants is astronomical, automated testing is a must. 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.
The Liar View bug pattern
Good debugging starts with good testing. And with the vast number of invariants that must be checked for a GUI program, automated testing is essential. But sometimes, despite having passed its test suite, 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 non-deterministic nature of the program is often the cause. But in the case of GUIs, there is another common cause -- the Liar View bug pattern.
The symptoms
Most GUI program tests, like program tests in general, follow this structure:
- Start the program.
- Check some aspect of its state.
- Attempt to modify the state.
- Check that the state was modified as intended.
But as I've mentioned, sometimes a manual inspection of the run-time program behavior will contradict the successful results of the tests: queues may appear on the screen containing elements that testing (supposedly) confirmed were deleted; and objects may contain stale data that was reportedly updated.
Bugs like these may cause us to question our sanity or, worse yet, fall into a Kantian 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).
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. The reason is that GUIs are generally designed with a model-view-controller (MVC) architecture. Indeed, the Swing class library builds this architecture into the structure of the GUI classes themselves.
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 one 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.
For example, consider the following simple GUI. It displays the contents of a list of elements as it is updated. 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 (see Resources).
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.
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.
Cures and preventions
One way to prevent this bug pattern is to check explicit properties of the view only after changing the state of the model. Although this technique limits the properties we can check to what is provided in the view, at least the assertions reflect what's really happening on screen.
For example, we could rewrite Controller.main as follows:
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 view rather than the model, it also checks the content of selected rows, rather than just the number of rows.
Another way to check the view directly is to use the Java Robot class (introduced in the Java 1.3 API -- see Resources) to actually automate the physical manipulation of a GUI with the mouse and keyboard.
The Robot 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. 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. Therefore, I recommend using the Robot class as a testing tool for mature GUIs whose view won't change very often. To test the logical aspects, call methods on the view like we do above.
A final caveat: 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.
Wrapup
Here's the breakdown of this week's bug pattern:
- 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 check aspects of the model rather than the view.
- Cures and preventions: Check aspects of the view.
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. Next time, we'll tackle a more subversive bug: saboteur data, data that is perfectly benign ... until it's accessed. Stay tuned!
Resources
About the author  | |  | Eric Allen has an A.B. in computer science and mathematics from Cornell University. He is currently the lead Java software developer at
Cycorp, Inc., and a part-time graduate student in the programming languages
team at Rice University. His research concerns the development of
formal semantic models of Java, and extensions of Java, both at the
source and bytecode levels. Currently, he is implementing a
source-to-bytecode compiler for the NextGen programming language, an
extension of Java with generic run-time types. Contact Eric at eallen@cyc.com. |
Rate this page
|  |