Testing a Save As Dialog in Java using Swing

0
91
9 min read

In this article, we will use an example from our demonstration application, Ikon Do It. The code from this article is in the packages jet.ikonmaker and jet.ikonmaker.test. Click here to download the code.

The Ikon Do It ‘Save as’ Dialog

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 figure shows the ‘Ikon Do It’ Save as dialog.

Testing a Save As Dialog in Java using Swing

Not all values are allowed as possible new names. Certain characters (such as ‘*’) are prohibited, as are names that are already used.

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. 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 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. Some of these are isEnabled(), runInEventThread(), and findNamedComponent(). 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 dialogs 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, this uses a class, Waiting. 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.

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();
}

LEAVE A REPLY

Please enter your comment!
Please enter your name here