Testing Swing components seems to fill many developers with fear. "How can I unit test this thing?" they often ask, "it's full of icky event handling code and troublesome painting...".
Frameworks such as Abbot are often the solution developers find for this problem. Essentially, many developers abandon the idea of writing regular unit tests for Swing components, and resort to "click simulators" which are frequently functional tests rather than unit tests.
There is a lot you can do just to test regular Swing components using bog-standard JUnit (or Test-NG) tests. Consider the following fairly basic Swing component.
package org.dubh.blog;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.JComponent;
/**
* A user interface widget.
*/
public final class Widget extends JComponent {
private List<WidgetListener> _listeners =
new CopyOnWriteArrayList<WidgetListener>();
private String _label = "";
public Widget() {
// Just for demonstration purposes.
addMouseListener( new MouseAdapter() {
public void mouseClicked( MouseEvent e ) {
if ( e.getButton() != MouseEvent.BUTTON1 ) return;
fireWidgetClicked();
}
});
}
/**
* Adds a listener.
*
* @param wl listener to add. Must not be null.
* @category Event handling
*/
public void addWidgetListener( WidgetListener wl ) {
if ( wl == null ) throw new NullPointerException( "wl is null" );
_listeners.add( wl );
}
/**
* Removes a listener.
*
* @param wl listener to remove.
* @category Event handling
*/
public void removeWidgetListener( WidgetListener wl ) {
_listeners.remove( wl );
}
/**
* Notifies listeners that the widget was clicked.
*
* @category Event handling
*/
protected void fireWidgetClicked() {
if ( _listeners.isEmpty() ) return;
WidgetEvent event = new WidgetEvent( this );
for ( WidgetListener l : _listeners ) {
l.widgetClicked( event );
}
}
/**
* Sets the label of the widget.
*
* @param label a widget label. Null is allowed, will be
* converted to the empty string.
*/
public void setLabel( String label ) {
if ( label == null ) label = "";
if ( label.equals( _label ) ) return;
String oldLabel = _label;
_label = label;
repaint();
firePropertyChange( "label", oldLabel, label );
}
/**
* Returns the label of the widget.
*
* @return the label of the widget. Never returns null.
*/
public String getLabel() {
return _label;
}
@Override
public void paintComponent(Graphics g) {
g.drawString( _label, 0, getHeight() );
}
}
We can start off with the really simple stuff. Let's at least test that we can make new instances of Widget.
public void testConstructor() {
new Widget();
}
Seems pretty dumb? Well... that constructor contains code, so maybe it could throw an exception. It'd be good to catch this when running our tests rather than when the code is out running in production. If you can do nothing else, at least constructing a class in a test is better than no test at all. But wait! There's a lot more we can do here. What about testing the pretty boring setLabel() / getLabel() set?
public void testGetAndSetLabel() {
Widget widget = new Widget();
// Test the initial state.
assertEquals( "", widget.getLabel() );
// Test setting and getting.
widget.setLabel( "Hello!" );
assertEquals( "Hello!", widget.getLabel() );
// Test edge case ( null label )
widget.setLabel( null );
assertEquals( "", widget.getLabel() );
}
OK.. this is a bit more meaty. We're testing some real assertions here. Now for the "hard" stuff. How can we test events? Well... Ignore the fact that these are events for now, and just look at the code for what it is. The management of listeners and the sending of events doesn't actually require AWT in most Swing components. Even if it did, we're not testing AWT here, we're testing Widget.
Let's use a common technique for testing listeners - introduce an inner class that provides a stub listener which simply tracks the events it receives:
private class WidgetListenerStub implements WidgetListener {
List<WidgetEvent> receivedEvents = new ArrayList<WidgetEvent>();
public void widgetClicked(WidgetEvent e) {
receivedEvents.add( e );
}
}
Now testing that we can add and remove widget listeners and receive events is easy:
public void testWidgetListeners() {
Widget widget = new Widget();
// Test firing when there are no listeners.
widget.fireWidgetClicked();
WidgetListenerStub l = new WidgetListenerStub();
widget.addWidgetListener( l );
widget.fireWidgetClicked();
assertEquals( 1, l.receivedEvents.size() );
assertSame( widget, l.receivedEvents.get( 0 ).getSource() );
widget.removeWidgetListener( l );
widget.fireWidgetClicked();
assertEquals( 1, l.receivedEvents.size() );
}
OK... what hasn't been tested yet? Well, we probably want to test multiple listeners and property change event notifications. Those are a pretty easy extrapolation from the example above.
One thing we haven't tested yet is that actually clicking in the widget with the mouse causes WidgetEvents to be fired. To do this, we don't have to actually test that a low level operating system mouse click event will cause our event to be fired. In other words, again, we're not testing AWT. We trust that Sun did a good job testing AWT already. No Robots required. Let's focus on events just at the level of our component, using a bit of reflection magic:
public void testMouseClickTriggersEvent() throws Exception {
Widget widget = new Widget();
WidgetListenerStub stub = new WidgetListenerStub();
widget.addWidgetListener( stub );
// Simulate a mouse click using reflection.
MouseEvent event = new MouseEvent( widget, MouseEvent.MOUSE_CLICKED, 0,
MouseEvent.BUTTON1_MASK, 10, 10, 1, false );
Method process = Component.class.getDeclaredMethod(
"processEvent", AWTEvent.class );
process.setAccessible( true );
process.invoke( widget, event );
assertEquals( 1, stub.receivedEvents.size() );
}
We're sending a mouse event directly to our widget component, and verifying that it causes the Widget to fire a WidgetEvent as expected. The really cool thing about this is that this does not require the component to be visible on screen, and it doesn't require native AWT event handling. Indeed, this test works perfectly fine even in headless mode (when the system property java.awt.headless=true, or you're running on a system with no graphics display).
One last thing we haven't tested: paintComponent(). This is arguably the toughest thing of all to test. Like the constructor example, it's at least a good idea to call this method in a test and make sure no exceptions occur. We can go a little bit further and actually test that paintComponent really painted something. More on that in my older blog entry, Unit Tesing: Coverage in Those Hard to Reach Places.
While frameworks like Abbot have their place, it's a lot easier to test Swing components with regular JUnit or Test-NG tests than you might think.
Brian,
ReplyDeleteYour are not entirely correct that you have to see the components when testing the UI. Take a look at the following entry in the FAQ:
http://abbot.sourceforge.net/doc/FAQ.shtml#q203
On Linux you can quite happily create virtual displays that are not bound to a monitor.
Cheers,
Gerard
Hi Brian,
ReplyDeletePretty interesting blog entry. Testing GUI components in isolation can be helpful. I don't agree with your assertion "no Robot is required" . It is easy to test "normal" (non-GUI) classes/components in isolation because the actual users of those classes are...other classes. We can say that the unit test simulates the usage of a normal class perfectly. The ultimate test for GUI-related stuff should include simulation of a user actually using the GUI component. An such user is a person. Hence we need a "Robot" for that.
Cheers,
Alex.
Hi Alex,
ReplyDeleteI agree that needs to be tested. But what you are describing is not a unit test.
Thanks,
Brian
And make sure all your tests happen on the EDT or you might run into some nasty surprises :)
ReplyDeleteHi Brian,
ReplyDeleteObviously I'm not describing a unit test. It is a functional test. What I am saying is that although unit test is useful, you need functional tests as well.
Cheers,
Alex.
I guess I've seen too many developers over the years kind of raise their hands and declare unit testing a bust on Swing and end up just writing functional tests instead because it's "easier".
ReplyDeleteI've found unit testing of swing code to be incredibly effective, and I'm less than impressed by some of the functional test frameworks out there (although I like FEST a lot of course :) ).
Brian
Brian,
ReplyDeleteIs it possible to test drag and drop, and focus tracking this way?
Bruce
In our book "Swing Extreme Testing" we develop a lot of utility code for testing user interfaces. Anyone seriously interested in thoroughly testing their Java desktop application should have a look. Anyhow, we have an improved Robot ("Cyborg") that makes clicking, dragging and dropping, using tables, lists etc. pretty straightforward. We develop another class ("UI") for safely interacting with Swing components from the test thread. TL.
ReplyDelete