Swing Extreme Testing : Learn Swing user interface testing strategy

The ‘Ikon Do It’ application has a Save as function that allows the icon on which we are currently working to be saved with another name. Activating the Save as button displays a very simple dialog for entering the new name. The following fi gure shows the ‘Ikon Do It’ Save as dialog. Not all values are allowed as possible new names. Certain characters (such as ‘*’) are prohibited, as are names that are already used.

also read:

In order to make testing easy, we implemented the dialog as a public class called SaveAsDialog, rather than as an inner class of the main user interface component. We might normally balk at giving such a trivial component its own class, but it is easier to test when written this way and it makes a good example. Also, once a simple version of this dialog is working and tested, it is possible to think of enhancements that would definitely make it too complex to be an inner class. For example, there could be a small status area that explains why a name is not allowed (the current implementation just disables the Ok button when an illegal name is entered, which is not very user-friendly).

The API for SaveAsDialog is as follows. Names of icons are represented by IkonName instances. A SaveAsDialog is created with a list of existing IkonNames. It is shown with a show() method that blocks until either Ok or Cancel is activated. If Ok is pressed, the value entered can be retrieved using the name() method. Here then are the public methods:

public class SaveAsDialog {
public SaveAsDialog( JFrame owningFrame,
SortedSet<IkonName> existingNames ) { ... }
/**
* Show the dialog, blocking until ok or cancel is activated.
*/
public void show() { ... }
/**
* The most recently entered name.
*/
public IkonName name() { ... }
/**
* Returns true if the dialog was cancelled.
*/
public boolean wasCancelled() { ... }
}

Note that SaveAsDialog does not extend JDialog or JFrame, but will use delegation like LoginScreen in Chapter 7. Also note that the constructor of SaveAsDialog does not have parameters that would couple it to the rest of the system. This means a handler interface as described in Chapter 6 is not required in order to make this simple class testable.

The main class uses SaveAsDialog as follows:

private void saveAs() {
SaveAsDialog sad = new SaveAsDialog( frame,
store.storedIkonNames() );
sad.show();
if (!sad.wasCancelled()) {
//Create a copy with the new name.
IkonName newName = sad.name();
Ikon saveAsIkon = ikon.copy( newName );
//Save and then load the new ikon.
store.saveNewIkon( saveAsIkon );
loadIkon( newName );
}
}

Outline of the Unit Test

The things we want to test are:

Initial settings:

  • The text field is empty.
  • The text field is a sensible size.
  • The Ok button is disabled.
  • The Cancel button is enabled.
  • The dialog is a sensible size.

Usability:

  • The Escape key cancels the dialog.
  • The Enter key activates the Ok button.
  • The mnemonics for Ok and Cancel work.

Correctness. The Ok button is disabled if the entered name:

  • Contains characters such as ‘*’, ‘\’, ‘/’.
  • Is just white-space.
  • Is one already being used.

API test: unit tests for each of the public methods.

As with most unit tests, our test class has an init() method for getting an object into a known state, and a cleanup() method called at the end of each test. The instance variables are:

  • A JFrame and a set of IkonNames from which the SaveAsDialog can be constructed.
  • A SaveAsDialog, which is the object under test.
  • A UserStrings and a UISaveAsDialog (listed later on) for manipulating the SaveAsDialog with keystrokes.
  • A ShowerThread, which is a Thread for showing the SaveAsDialog. This is listed later on.

The outline of the unit test is:

public class SaveAsDialogTest {
private JFrame frame;
private SaveAsDialog sad;
private IkonMakerUserStrings =
IkonMakerUserStrings.instance();
private SortedSet<IkonName> names;
private UISaveAsDialog ui;
private Shower shower;
...
private void init() {
...
}
private void cleanup() {
...
}
private class ShowerThread extends Thread {
...
}
}

UI Helper Methods

A lot of the work in this unit test will be done by the static methods in our helper class, UI. We looked at some of these (isEnabled(), runInEventThread(), and findNamedComponent()) in Chapter 8. The new methods are listed now, according to their function.

Dialogs

If a dialog is showing, we can search for a dialog by name, get its size, and read its title:

public final class UI {
...
/**
* Safely read the showing state of the given window.
*/
public static boolean isShowing( final Window window ) {
final boolean[] resultHolder = new boolean[]{false};
runInEventThread( new Runnable() {
public void run() {
resultHolder[0] = window.isShowing();
}
} );
return resultHolder[0];
}
/**
* The first found dialog that has the given name and
* is showing (though the owning frame need not be showing).
*/
public static Dialog findNamedDialog( String name ) {
Frame[] allFrames = Frame.getFrames();
for (Frame allFrame : allFrames) {
Window[] subWindows = allFrame.getOwnedWindows();
for (Window subWindow : subWindows) {
if (subWindow instanceof Dialog) {
Dialog d = (Dialog) subWindow;
if (name.equals( d.getName() )
&& d.isShowing()) {
return (Dialog) subWindow;
}
}
}
}
return null;
}
/**
* Safely read the size of the given component.
*/
public static Dimension getSize( final Component component ) {
final Dimension[] resultHolder = new Dimension[]{null};
runInEventThread( new Runnable() {
public void run() {
resultHolder[0] = component.getSize();
}
} );
return resultHolder[0];
}
/**
* Safely read the title of the given dialog.
*/
public static String getTitle( final Dialog dialog ) {
final String[] resultHolder = new String[]{null};
runInEventThread( new Runnable() {
public void run() {
resultHolder[0] = dialog.getTitle();
}
} );
return resultHolder[0];
} ...
}

Getting the Text of a Text Field

The method is getText(), and there is a variant to retrieve just the selected text:

//... from UI
/**
* Safely read the text of the given text component.
*/
public static String getText( JTextComponent textComponent ) {
return getTextImpl( textComponent, true );
}
/**
* Safely read the selected text of the given text component.
*/
public static String getSelectedText(
JTextComponent textComponent ) {
return getTextImpl( textComponent, false );
}
private static String getTextImpl(
final JTextComponent textComponent,
final boolean allText ) {
final String[] resultHolder = new String[]{null};
runInEventThread( new Runnable() {
public void run() {
resultHolder[0] = allText ? textComponent.getText() :
textComponent.getSelectedText();
}
} );
return resultHolder[0];
}

Frame Disposal

In a lot of our unit tests, we will want to dispose of any dialog or frames that are still showing at the end of a test. This method is brutal but effective:

//... from UI
public static void disposeOfAllFrames() {
Runnable runnable = new Runnable() {
public void run() {
Frame[] allFrames = Frame.getFrames();
for (Frame allFrame : allFrames) {
allFrame.dispose();
}
}
};
runInEventThread( runnable );
}

Unit Test Infrastructure

Having seen the broad outline of the test class and the UI methods needed, we can look closely at the implementation of the test. We’ll start with the UI Wrapper class and the init() and cleanup() methods.

The UISaveAsDialog Class

UISaveAsDialog has methods for entering a name and for accessing the dialog, buttons, and text field. The data entry methods use a Cyborg, while the component accessor methods use UI:

public class UISaveAsDialog {
Cyborg robot = new Cyborg();
private IkonMakerUserStrings us =
IkonMakerUserStrings.instance();
protected Dialog namedDialog;
public UISaveAsDialog() {
namedDialog = UI.findNamedDialog(
SaveAsDialog.DIALOG_NAME );
Waiting.waitFor( new Waiting.ItHappened() {
public boolean itHappened() {
return nameField().hasFocus();
}
}, 1000 );
}
public JButton okButton() {
return (JButton) UI.findNamedComponent(
IkonMakerUserStrings.OK );
}
public Dialog dialog() {
return namedDialog;
}
public JButton cancelButton() {
return (JButton) UI.findNamedComponent(
IkonMakerUserStrings.CANCEL );
}
public JTextField nameField() {
return (JTextField) UI.findNamedComponent(
IkonMakerUserStrings.NAME );
}
public void saveAs( String newName ) {
enterName( newName );
robot.enter();
}
public void enterName( String newName ) {
robot.selectAllText();
robot.type( newName );
}
public void ok() {
robot.altChar( us.mnemonic( IkonMakerUserStrings.OK ) );
}
public void cancel() {
robot.altChar( us.mnemonic( IkonMakerUserStrings.CANCEL ) );
}
}

A point to note here is the code in the constructor that waits for the name text field to have focus. This is necessary because the inner workings of Swing set the focus within a shown modal dialog as a separate event. That is, we can’t assume that showing the dialog and setting the focus within it happen within a single atomic event. Apart from this wrinkle, all of the methods of UISaveDialog are straightforward applications of UI methods.

The ShowerThread Class

Since SaveAsDialog.show() blocks, we cannot call this from our main thread; instead we spawn a new thread. This thread could just be an anonymous inner class in the init() method:

private void init() {
//Not really what we do...
//setup...then launch a thread to show the dialog.
//Start a thread to show the dialog (it is modal).
new Thread( "SaveAsDialogShower" ) {
public void run() {
sad = new SaveAsDialog( frame, names );
sad.show();
}
}.start();
//Now wait for the dialog to show...
}

The problem with this approach is that it does not allow us to investigate the state of the Thread that called the show() method. We want to write tests that check that this thread is blocked while the dialog is showing.

Our solution is a simple inner class:
private class ShowerThread extends Thread {
private boolean isAwakened;
public ShowerThread() {
super( "Shower" );
setDaemon( true );
}
public void run() {
Runnable runnable = new Runnable() {
public void run() {
sad.show();
}
};
UI.runInEventThread( runnable );
isAwakened = true;
}
public boolean isAwakened() {
return Waiting.waitFor( new Waiting.ItHappened() {
public boolean itHappened() {
return isAwakened;
}
}, 1000 );
}
}

The method of most interest here is isAwakened(), which waits for up to one second for the awake flag to have been set. We’ll look at tests that use this isAwakened() method later on. Another point of interest is that we’ve given our new thread a name (by the call super(“Shower”) in the constructor). It’s really useful to give each thread we create a name, for reasons that will be discussed in Chapter 20.

The init() Method

The job of the init() method is to create and show the SaveAsDialog instance so that it can be tested:

private void init() {
//Note 1
names = new TreeSet<IkonName>();
names.add( new IkonName( "Albus" ) );
names.add( new IkonName( "Minerva" ) );
names.add( new IkonName( "Severus" ) );
names.add( new IkonName( "Alastair" ) );
//Note 2
Runnable creator = new Runnable() {
public void run() {
frame = new JFrame( "SaveAsDialogTest" );
frame.setVisible( true );
sad = new SaveAsDialog( frame, names );
}
};
UI.runInEventThread( creator );
//Note 3
//Start a thread to show the dialog (it is modal).
shower = new ShowerThread();
shower.start();
//Note 4
//Wait for the dialog to be showing.
Waiting.waitFor( new Waiting.ItHappened() {
public boolean itHappened() {
return UI.findNamedFrame(
SaveAsDialog.DIALOG_NAME ) != null;
}
}, 1000 );
//Note 5
ui = new UISaveAsDialog();
}

Now let’s look at some of the key points in this code.

  • Note 1: In this block of code we create a set of IkonNames with which our SaveAsDialog can be created.
  • Note 2: It’s convenient to create and show the owning frame and create the SaveAsDialog in a single Runnable. An alternative would be to create and show the frame with a UI call and use the Runnable just for creating the SaveAsDialog.
  • Note 3: Here we start our Shower, which will call the blocking show() method of SaveAsDialog from the event thread.
  • Note 4: Having called show() via the event dispatch thread from our Shower thread, we need to wait for the dialog to actually be showing on the screen. The way we do this is to search for a dialog that is on the screen and has the correct name.
  • Note 5: Once the SaveAsDialog is showing, we can create our UI Wrapper for it.

The cleanup() Method

The cleanup() method closes all frames in a thread-safe manner:

private void cleanup() {
UI.disposeOfAllFrames();
}

The Unit Tests

We’ve now done all the hard work of building an infrastructure that will make our tests very simple to write. Let’s now look at these tests.

The Constructor Test

A freshly constructed SaveAsDialog should be in a known state, and we need to check the things we listed at the start of this chapter.

public boolean constructorTest() {
//Note 1
init();
//Note 2
//Check the title.
assert UI.getTitle( ui.dialog() ).equals(
us.label( IkonMakerUserStrings.SAVE_AS ) );
//Note 3
//Check the size.
Dimension size = UI.getSize( ui.dialog() );
assert size.width > 60;
assert size.width < 260;
assert size.height > 20;
assert size.height < 200;
//Note 4
//Name field initially empty.
assert UI.getText( ui.nameField() ).equals( "" );
//Name field a sensible size.
Dimension nameFieldSize = UI.getSize( ui.nameField() );
assert nameFieldSize.width > 60;
assert nameFieldSize.width < 260;
assert nameFieldSize.height > 15;
assert nameFieldSize.height < 25;
//Ok not enabled.
assert !UI.isEnabled( ui.okButton() );
//Cancel enabled.
assert UI.isEnabled( ui.cancelButton() );
//Type in some text and check that the ok button is now enabled.
ui.robot.type( "text" );
assert UI.isEnabled( ui.okButton() );
cleanup();
return true;
}

Let’s now look at the noteworthy parts of this code.

  • Note 1: In accordance with the rules for GrandTestAuto (see Chapter 19), the test is a public method, returns a boolean, and has name ending with “Test”. As with all of the tests, we begin with the init() method that creates and shows the dialog. After the body of the test, we call cleanup() and return true. If problems are found, they cause an assert exception.
  • Note 2: The UI Wrapper gives us the dialog, and from this we can check the title. It is important that we are using the UI class to get the title of the dialog in a thread-safe manner.
  • Note 3: Here we are checking the size of dialog is reasonable. The actual allowed upper and lower bounds on height and width were found simply by trial and error.
  • Note 4: Although the UI Wrapper gives us access to the name fi eld and the buttons, we must not interrogate them directly. Rather, we use our UI methods to investigate them in a thread-safe manner.

The wasCancelled() Test

The fi rst of our API tests is to check the wasCancelled() method. We will basically do three investigations. The fi rst test will call wasCancelled() before the dialog has been cancelled. The second test will cancel the dialog and then call the method. In the third test we will enter a name, cancel the dialog, and then call wasCancelled(). There will be a subtlety in the test relating to the way that the Cancel button operates. The button is created in the constructor of the SaveAsDialog:

//From the constructor of SaveAsDialog
...
AbstractAction cancelAction = new AbstractAction() {
public void actionPerformed( ActionEvent a ) {
wasCancelled = true;
dialog.dispose();
}
};
JButton cancelButton = us.createJButton( cancelAction,
IkonMakerUserStrings.CANCEL );
buttonBox.add( Box.createHorizontalStrut( 5 ) );
buttonBox.add( cancelButton );
...

The action associated with the button sets the wasCancelled instance variable of the SaveAsDialog. The wasCancelled() method simply returns this variable:
From SaveAsDialog

public boolean wasCancelled() {
return wasCancelled;
}
It follows that the wasCancelled() method is not thread-safe because the value it returns is set in the event thread. Therefore, in our test, we need to call this method from the event thread. To do this, we put a helper method into our test class:
1
//From SaveAsDialogTest
1
private boolean wasCancelled() {
final boolean[] resultHolder = new boolean[1];
UI.runInEventThread( new Runnable() {
public void run() {
resultHolder[0] = sad.wasCancelled();
}
} );
return resultHolder[0];
}

Our wasCancelledTest() then is:

public boolean wasCancelledTest() {
//When the ok button has been pressed.
init();
assert !wasCancelled();
ui.saveAs( "remus" );
assert !UI.isShowing( ui.dialog() );
assert !wasCancelled();
cleanup();
//Cancel before a name has been entered.
init();
ui.cancel();
assert !UI.isShowing( ui.dialog() );
assert wasCancelled();
cleanup();
//Cancel after a name has been entered.
init();
ui.robot.type( "remus" );
ui.cancel();
assert !UI.isShowing( ui.dialog() );
assert wasCancelled();
cleanup();
return true;
}

There are three code blocks in the test, corresponding to the cases discussed above, with each block of code being very simple and making use of the wasCancelled() helper method.

The name() Test

Like the wasCancelled() method, the name() method is not thread-safe, so our test class needs another boilerplate helper method:

//From SaveAsDialogTest
private IkonName enteredName() {
final IkonName[] resultHolder = new IkonName[1];
UI.runInEventThread( new Runnable() {
public void run() {
resultHolder[0] = sad.name();
}
} );
return resultHolder[0];
}

Using this, we can write our nameTest():

public boolean nameTest() {
init();
//Note 1
assert enteredName() == null;
//Note 2
ui.robot.type( "remus" );
assert enteredName().equals( new IkonName( "remus" ) );
//Note 3
ui.ok();
assert enteredName().equals( new IkonName( "remus" ) );
cleanup();
return true;
}

The main points of this test are as follows.

  • Note 1: Here we simply check that with no value entered into the text field, the method returns null. This could have gone into the constructor test.
  • Note 2: UISaveAsDialog has an enterName() method that types in the name and then presses Enter. In this test we want to type in a name, but not yet activate Ok by pressing Enter. So we use the Cyborg in the UISaveDialog to just type the name, and then we check the value of name(). This part of the test helps to define the method SaveAsDialog.name() by establishing that a value is returned even when the Ok button has not been activated.
  • Note 3: Here we are just testing that activating the Ok button has no effect on the value of name(). Later, we will also test whether the Ok button disposes the dialog. It would be tempting to write some reflective method that made methods like name(), wasCancelled(), and enteredName() one-liners. However, that would make these examples much harder to understand. A bigger problem, though, is that we would lose compile-time checking: refl ection breaks at runtime when we rename methods.

The show() Test

Our tests have used the show() method because it is used in init(). So we can be sure that show() actually does bring up the SaveAsDialog user interface. What we will check in showTest() is that the show() method blocks the calling thread.

public boolean showTest() {
init();
assert !shower.isAwakened();
ui.cancel();
assert shower.isAwakened();
cleanup();
init();
assert !shower.isAwakened();
ui.saveAs( "ikon" );
assert shower.isAwakened();
cleanup();
return true;
}

In the first sub-test, we check that cancellation of the SaveAsDialog wakes the launching thread. In the second sub-test, we check that activation of the Ok button wakes the launching thread.

The Data Validation Test

The Ok button of the SaveAsDialog should only be enabled if the name that has been entered is valid. A name can be invalid if it contains an illegal character, or if it has already been used.

To test this behavior, we type in an invalid name, check that the Ok button is not enabled, then type in a valid name and test that it now is enabled:

ui.enterName( "*" );
assert !UI.isEnabled( ui.okButton() );
ui.enterName( "remus");
assert UI.isEnabled( ui.okButton() );

Our validateDataTest() started with a single block of code like that above. This block of code was copied and varied with different invalid strings:

public boolean validateDataTest() {
//First check names that have illegal characters.
init();
ui.robot.type( " " );
assert !UI.isEnabled( ui.okButton() );
ui.enterName( "remus");
assert UI.isEnabled( ui.okButton() );
ui.enterName( "*" );
assert !UI.isEnabled( ui.okButton() );
ui.enterName( "remus");
assert UI.isEnabled( ui.okButton() );
...
//Seven more blocks just like these.
...
cleanup();
return true;
}

Later on, this was refactored to:

public boolean validateDataTest() {
init();
//First check names that have illegal characters.
checkOkButton( " " );
checkOkButton( "*" );
checkOkButton( "/" );
checkOkButton( "\\" );
//Now names that are already there.
Case Study: Testing a ‘Save as’ Dialog
[ 136 ]
checkOkButton( "albus" );
checkOkButton( "Albus" );
checkOkButton( "ALBUS" );
checkOkButton( "MINERVA" );
cleanup();
return true;
}
private void checkOkButton( String name ) {
ui.enterName( name );
assert !UI.isEnabled( ui.okButton() );
ui.enterName( "remus" );
assert UI.isEnabled( ui.okButton() );
}

The refactored code is much more readable, contains fewer lines, and does not contain the useless repeated test for the illegal string "*" that the original does. This illustrates a good point about writing test code. Because a lot of tests involve testing the state of an object against various simple inputs, it is very easy for such code to end up being unreadable and horribly repetitive. We should always be looking to refactor such code. Not only will this make the tests easier to maintain, it also makes the code more interesting to write. As we argued in Chapter 1, we should apply the same quality standards to our test code that we do to our production code.

The Usability Test

Typically, a simple dialog should be able to be cancelled with the Escape key, and the Enter key should activate the Ok button. In this test, we check these usability requirements and also check that tabbing to the buttons and activating them with the space key works as expected.

public boolean usabilityTest() {
//Check that 'escape' cancels.
init();
ui.robot.escape();
assert !UI.isShowing( ui.dialog() );
assert wasCancelled();
cleanup();
//Check activating the cancel button when it has focus.
init();
ui.robot.tab();//Only one tab needed as ok is not enabled.
ui.robot.activateFocussedButton();
assert !UI.isShowing( ui.dialog() );
assert wasCancelled();
cleanup();
//Check that 'enter' is like 'ok'.
init();
ui.robot.type( "remus" );
ui.robot.enter();
assert !UI.isShowing( ui.dialog() );
assert !wasCancelled();
assert enteredName().equals( new IkonName( "remus" ) );
cleanup();
//Check activating the ok button when it has focus.
init();
ui.robot.type( "remus" );
ui.robot.tab();
ui.robot.activateFocussedButton();
assert !UI.isShowing( ui.dialog() );
assert !wasCancelled();
assert enteredName().equals( new IkonName( "remus" ) );
cleanup();
return true;
}

Summary

This chapter and the two preceding ones have given us all the principles we need to write solid, automated, and fairly painless tests for our user interfaces. The key points are:

  • We should write a UI Wrapper class for the class we are testing and use it to manipulate the test objects.
  • All creation and setup of components must be done in the event thread.
  • All querying of the state of components must be done in a thread-safe manner.
  • Any variable, either in a user interface or in a handler, that is set from the event thread needs to be read in a thread-safe manner.
  • The class UI contains a lot of methods for making it easy to follow these principles. In the next chapter we'll see more of this useful class.

Comments

comments

About Krishna Srinivasan

He is Founder and Chief Editor of JavaBeat. He has more than 8+ years of experience on developing Web applications. He writes about Spring, DOJO, JSF, Hibernate and many other emerging technologies in this blog.

Speak Your Mind

*