2007-03-03

Unit testing Swing components - impossible?

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.
    addMouseListenernew MouseAdapter() {
      public void mouseClickedMouseEvent 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 addWidgetListenerWidgetListener wl ) {
    if wl == null throw new NullPointerException"wl is null" );
    
    _listeners.addwl );
  }
  
  /**
   * Removes a listener.
   
   @param wl listener to remove.
   * @category Event handling
   */
  public void removeWidgetListenerWidgetListener wl ) {
    _listeners.removewl );
  }
  
  /**
   * Notifies listeners that the widget was clicked.
   
   * @category Event handling
   */
  protected void fireWidgetClicked() {
    if _listeners.isEmpty() ) return;
    
    WidgetEvent event = new WidgetEventthis );
    for WidgetListener l : _listeners ) {
      l.widgetClickedevent );
    }
  }
  
  /**
   * Sets the label of the widget.
   
   @param label a widget label. Null is allowed, will be 
   *    converted to the empty string.
   */
  public void setLabelString 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.setLabelnull );
    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);
    }
  }

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);
    
    widget.fireWidgetClicked();
    assertEquals1, l.receivedEvents.size() );
    assertSamewidget, l.receivedEvents.get).getSource() );
    
    widget.removeWidgetListener);
    widget.fireWidgetClicked();
    assertEquals1, 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.addWidgetListenerstub );
    
    // Simulate a mouse click using reflection.
    MouseEvent event = new MouseEventwidget, MouseEvent.MOUSE_CLICKED, 0
      MouseEvent.BUTTON1_MASK, 10101false );
    Method process = Component.class.getDeclaredMethod
      "processEvent", AWTEvent.class );
    process.setAccessibletrue );
    process.invokewidget, event );
    
    assertEquals1, 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.

AddThis Social Bookmark Button

8 comments:

  1. Brian,

    Your 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

    ReplyDelete
  2. Hi Brian,

    Pretty 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.

    ReplyDelete
  3. Hi Alex,

    I agree that needs to be tested. But what you are describing is not a unit test.

    Thanks,
    Brian

    ReplyDelete
  4. And make sure all your tests happen on the EDT or you might run into some nasty surprises :)

    ReplyDelete
  5. Hi Brian,

    Obviously 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.

    ReplyDelete
  6. 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".

    I'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

    ReplyDelete
  7. Brian,

    Is it possible to test drag and drop, and focus tracking this way?

    Bruce

    ReplyDelete
  8. 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