| January, 2000 |
| People who unit-test, even many who unit-test in Extreme Programming, don't necessarily test the user interface. You can use JUnit to assist in this testing, however. This paper will work through a small but plausible example, giving the flavor of testing and programming using JUnit. This paper is part 2, but can be read on its own; part 1 developed the model. |
We'll develop it in the XP style, working back and forth between testing
and coding. The code fragments will reflect this: tests will be on the
left side of the page, and application code on the right.
|
In testing and development of the GUI, I don't mind depending on the interfaces of the model; I'm less happy when I have to depend on its concrete classes.
So, we'll create testWidgetsPresent(). To make this work, we need a panel for the overall screen ("SearchPanel"), the label ("searchLabel"), a textfield for entering the query ("queryField"), a button ("findButton"), and a table for the results ("resultTable"). We'll let these widgets be package-access, so our test can see them.
public void testWidgetsPresent() {
SearchPanel panel = new SearchPanel();
assertNotNull(panel.searchLabel);
assertNotNull(panel.queryField);
assertNotNull(panel.findButton);
assertNotNull(panel.resultTable);
}
|
The test fails to compile. (Of course - we haven't created SearchPanel yet.) So, create class SearchPanel with its widget fields, so we can compile. Don't initialize the widgets yet - run the test and verify that it fails. (It's good practice to see the test fail once; this helps assure you that it captures failures, and lets you ensure that the testing is driving the coding.) Code enough assignments to make the test pass.
Things to notice:
We can make another test, to verify that the widgets are set up correctly:
public void testInitialContents() {
SearchPanel sp = new SearchPanel();
assertEquals("Search:", sp.searchLabel.getText());
assertEquals("", sp.queryField.getText());
assertEquals("Find", sp.findButton.getText());
assert("Table starts empty", sp.resultTable.getRowCount() == 0);
}
|
Run this test, and we're ok.
At this point, our SearchPanel code looks like this:
|
We could go in either of two directions: push on developing the user interface, or its interconnection with searching. The urge to "see" the interface is strong, but we'll resist it in favor of interconnection.
We'll give our panel two methods, getSearcher() and setSearcher(), that will associate a Searcher with the panel. This decision lets us write another test:
public void testSearcherSetup() {
Searcher s = new Searcher() {
public Result search(Query q) {return null;}
};
SearchPanel panel = new SearchPanel();
assert ("Searcher not set", panel.getSearcher() != s);
panel.setSearcher(s);
assert("Searcher now set", panel.getSearcher() == s);
}
|
The compile fails, so bounce over to SearchPanel, add the methods, run the tests again, they fail; implement the set/get methods, and the test passes.
The panel still can't do much, but now we can associate a Searcher with it.
Because this is a unit test, I don't want to depend on the real Searcher implementations: I'd rather create my own for testing purposes. This lets me control behavior in a fine-grained way. Here, I'll create a new Searcher called TestSearcher. We'll have the query string be an integer, which will tell how many items to return. We'll name the items "a0" (for first author), "t1" (second title), etc.
But first... a test. (Notice this is a test of our testing class, not of our GUI.)
public void testTestSearcher() {
assertEquals(new Query("1").getValue(), "1");
Document d = new TestDocument(1);
assertEquals("y1", d.getYear());
Result tr = new TestResult(2);
assert(tr.getCount() == 2);
assertEquals("a0", tr.getItem(0).getAuthor());
TestSearcher ts = new TestSearcher();
tr = ts.find(ts.makeQuery("2"));
assert("Result has 2 items", tr.getCount() == 2);
assertEquals("y1", tr.getItem(1).getYear());
}
|
Go through the usual compile/fail cycle, and create the test classes, starting with TestDocument:
public class TestDocument implements Document {
int count;
public TestDocument(int n) {count = n;}
public String getAuthor() {return "a" + count;}
public String getTitle() {return "t" + count;}
public String getYear() {return "y" + count;}
}
|
The TestResult class has a constructor that takes an integer telling how many rows should be present:
public class TestResult implements Result {
int count;
public TestResult(int n) {count = n;}
public int getCount() {return count;}
public Document getItem(int i) {return new TestDocument(i);}
}
|
TestSearcher uses the number value of the query string to create the result:
public class TestSearcher implements Searcher {
public Result find(Query q) {
int count = 0;
try {count = Integer.parseInt(q.getValue());}
catch (Exception ignored) {}
return new TestResult(count);
}
}
|
Run the test again, and it passes.
public void test0() {
SearchPanel sp = new SearchPanel();
sp.setSearcher (new TestSearcher());
sp.queryField.setText("0");
sp.findButton.doClick();
assert("Empty result", sp.resultTable.getRowCount() == 0);
}
|
At last, we're using the GUI: setting text fields, clicking buttons, etc.
We run the test - and it passes! This means we already have a working solution - if our searcher always returns 0 items.
We move on:
public void test1() {
SearchPanel sp = new SearchPanel();
sp.setSearcher (new TestSearcher());
sp.queryField.setText("1");
sp.findButton.doClick();
assert("1-row result", sp.resultTable.getRowCount() == 1);
assertEquals(
"a0",
sp.resultTable.getValueAt(0,0).toString());
}
|
Now we fail, because we don't have any event-handling code on the button.
When the button is clicked, we want to form the string in the text field
into a query, then let the searcher find us a result we can display in
the table. However, we have a problem in matching types:
the Searcher gives us a Result, but the table in our GUI needs a TableModel.
We need an adapter to make the interfaces conform.
|
When this fails to compile, stub out a dummy implementation:
|
Test0() still passes, and test1() still fails.
The adapter is straightforward to write, but we begin by writing a test.
public void testResultTableAdapter() {
Result result = new TestResult(2);
ResultTableAdapter rta = new ResultTableAdapter(result);
assertEquals("Author", rta.getColumnName(0));
assertEquals("Title", rta.getColumnName(1));
assertEquals("Year", rta.getColumnName(2));
assert("3 columns", rta.getColumnCount() == 3);
assert("Row count=2", rta.getRowCount() == 2);
assertEquals("a0", rta.getValueAt(0,0).toString());
assertEquals("y1", rta.getValueAt(1,2).toString());
}
|
The test fails because the dummy implementation doesn't do anything.
Bounce over and implement the ResultTableAdapter. Change it to be a subclass of AbstractTableModel (instead of DefaultTableModel), then implement the column names, column and row counts, and finally getValueAt().
|
This test (testResultTableAdapter) should pass, and so should test1().
What else can give you problems? One possible problem occurs when we do a sequence of queries - can we get "leftovers"? For example, a query returning 5 items followed by a query returning 3 items should only have 3 items in the table. (If the table were improperly cleared, we might see the last two items of the previous query.)
We can test a sequence of queries:
public void testQuerySequenceForLeftovers() {
SearchPanel sp = new SearchPanel();
sp.setSearcher (new TestSearcher());
sp.queryField.setText("5");
sp.findButton.doClick();
assert(sp.resultTable.getRowCount() == 5);
sp.queryField.setText("3");
sp.findButton.doClick();
assert(sp.resultTable.getRowCount() == 3);
}
|
This test passes.
To make this test run, we need to put our panel in a frame or window. (Components don't have their screen locations set until their containing window is created.)
public void testRelativePosition() {
SearchPanel sp = new SearchPanel();
JFrame display = new JFrame("test");
display.getContentPane().add(sp);
display.setSize(500,500);
display.setVisible(true);
//try {Thread.sleep(3000);} catch (Exception ex) {}
assert ("label left-of query",
sp.searchLabel.getLocationOnScreen().x
< sp.queryField.getLocationOnScreen().x);
assert ("query left-of button",
sp.queryField.getLocationOnScreen().x
< sp.findButton.getLocationOnScreen().x);
assert ("query above table",
sp.queryField.getLocationOnScreen().y
< sp.resultTable.getLocationOnScreen().y);
}
|
The test fails, as we haven't done anything to put widgets on the panel. (You can un-comment the sleep() if you want to see it on-screen.)
To implement panels, I usually do a screen design that shows the intermediate panels and layouts:
Now we can lay out the panel:
|
Compile, test, and it works.
We've successfully implemented our panel!
|
[Written 1-3-2000; revised 2-1-2000; re-titled and revised 2-4-2000. Linked to xp0001.zip, 10-26-00.]
Copyright 2000, William C. Wake ....... Email ....... Site Map ....... Top of Site