On the first page of this lesson we developed a utility for traversing an application’s physical window hierarchy in order to find and manipulate individual components. On this page we will talk about using the utility to improve the JUnit tests for those elements of our application that work with dialogs.
Page 1: Cartesian Plane Lesson 14: GUI Analysis
Page 2: Cartesian Plane Lesson 14: GUI Testing: ComponentFinder JUnit Test
GitHub repository: Cartesian Plane Part 14
Previous lesson: Cartesian Plane Lesson 13: More Extensions
ItemSelectionDialogTest
Previously, our JUnit test for ItemSelectionDialog had two test methods: testShowOK and testShowCancel. These used Robot to emulate the operator’s keyboard to select items, and choose either the OK button (by pretending to press the enter key) or the Cancel button (by pretending to press the escape key). This strategy has some inherent problems; notably, the operator can’t touch the keyboard or the mouse during the test, otherwise the test is likely to malfunction. In the revised tests, most of the Robot logic will be replaced by direct manipulation of the GUI.
However, we’re not quite done with Robot. If you recall, when writing ItemSelectionDialog we took steps to enure that an enter key press automatically selects the OK button, and pressing the escape key selects the cancel button. We will continue to use Robot to test this feature.
Something we neglected in the original version of our test was validating that the list of items held by the dialog is correct. With ComponentFinder we can easily remedy this problem. In addition, we’ve made a modification to the ItemSelectionDialog class that needs to be tested; to wit, if a dialog contains an empty list, the OK button should be disabled. Conversely, if a dialog contains a non-empty list, the OK button should be enabled.
Our original test has an instance variable, mainDialog, to hold an ItemSelectionDialog object. We’re going to be manipulating that variable from a @BeforeAll method, which must be static, so mainDialog will have to be recast as a class variable. We’re also going to add class variables to hold the JList and the two JButton components of the dialog, and to store the actual list of items that the dialog contains. We’ll initialize the variables in the @BeforeAll method.
We’ll start by examining and annotating our class variables, @BeforeAll method, and a helper method to locate push buttons, getButton.
Review: the code uses the ? annotation in generic declarations, for example JList<?> jList. We’ve seen this before; the question mark (called a wildcard) simply means any type. Why use it? Because it doesn’t hurt anything, and it saves space.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | class ItemSelectionDialogTest { private static JList<?> jList; private static List<?> actListItems; private static JButton okButton; private static JButton cancelButton; private static final String[] names = { "Sally", "Manny", "Jane", "Moe", "Anapurna", "Jack", "Alice", "Tweedledee", "Elizabeth", "Tweedledum", }; private static final ItemSelectionDialog mainDialog = new ItemSelectionDialog( "JUnit Test", names ); /** This variable set by show() method. */ private int selection = 0; private static JButton getButton( ComponentFinder finder, String text ) { Predicate<JComponent> pred = ComponentFinder.getButtonPredicate( text ); JComponent comp = finder.find( pred ); assertNotNull( comp ); assertTrue( comp instanceof JButton ); assertEquals( text, ((JButton)comp).getText() ); return (JButton)comp; } @BeforeAll public static void beforeAll() { ComponentFinder finder = new ComponentFinder( true, false, false ); okButton = getButton( finder, "OK" ); cancelButton = getButton( finder, "Cancel" ); JComponent comp = finder.find( c -> c instanceof JList ); assertNotNull( comp ); assertTrue( comp instanceof JList ); jList = (JList<?>)comp; ListModel<?> model = jList.getModel(); int size = model.getSize(); actListItems = IntStream.range( 0, size ) .mapToObj( model::getElementAt ) .collect( Collectors.toList() ); } // ... } |
- Line 3: Holds the JList component of the dialog.
- Line 4: Holds the actual list of items stored in the dialog.
- Line 5: Holds the dialog’s OK button.
- Line 6: Holds the dialog’s Cancel button.
- Lines 8-11: The list of items we expect to be stored in the dialog.
- Lines 13-14: The dialog used for most of the tests.
- Line 17: The result from the last use of the principal dialog. Set in showMethod.
- Lines 19-28: Help method to get a JButton with a specific label.
- Lines 21-22: Gets the appropriate Predicate<JComponent>.
- Line 23: Obtains the desired component.
- Line 24: Verifies that the result of the previous operation was non-null.
- Line 25: Verifies that the result of the previous operation was a JButton.
- Line 26: Verifies that the JButton has the correct label.
- Lines 33-34: Obtains a ComponentFinder with top-level parameters canBeDialog=true, canBeFrame=false and mustBeVisible=false.
- Lines 35-36: Gets the OK and Cancel buttons.
- Line 37: Gets the dialog’s JList.
- Lines 38-39: Validates the result of the previous operation.
- Line 40: Stores the JList.
- Line 42: Gets the JList’s model.
- Line 43: Gets the number of elements in the model’s list (the size of the list).
- Line 44: Generates indices from 0 to size – 1.
- Line 45: Gets the string at each index.
- Line 46: Saves the elements from the previous operation as a list.
■ Revised Test Method: testShowOK
This method selects an item in the principal dialog’s list, then dismisses it by pushing the OK button, and verifies that the correct index was returned by the dialog’s show method. Instead of Robot, our revised method:
- Uses JList.setSelectedItem(int) to select a list item.
- Uses JButton.doClick() to activate the OK button.
@Test
void testShowOK()
{
Thread thread = startDialog();
int expSelection = 2;
jList.setSelectedIndex( expSelection );
SwingUtilities.invokeLater( () -> okButton.doClick() );
Utils.join( thread );
assertEquals( expSelection, selection );
}
*Review: As discussed on Page 1 of this lesson, The Event Dispatch Thread, the doClick() must be executed on the EDT. SwingUtilities.invokeLater is how we make this happen.
■ Revised Test Method: testShowCancel
Instead of Robot, the revised method uses JButton.doClick() to activate the Cancel button.
@Test
void testShowCancel()
{
Thread thread = startDialog();
SwingUtilities.invokeLater( () -> cancelButton.doClick() );
Utils.join( thread );
assertTrue( selection < 0 );
}
■ New Test Method: testEnterKeyboardAction
■ New Test Method: testEscapeKeyboardAction
These methods will use Robot to simulate enter and escape key presses in order to verify that the OK and Cancel buttons are activated in response.
@Test
public void testEnterKeyboardAction()
throws AWTException
{
Thread thread = startDialog();
RobotAssistant robot = new RobotAssistant();
int expSelection = 2;
jList.setSelectedIndex( expSelection );
robot.type( "", KeyEvent.VK_ENTER );
Utils.join( thread );
assertEquals( expSelection, selection );
}
■ New Test Method: testDialogContent
This method verifies that the length of the names array, the data source for the ItemSelectionDialog, matches the length of actListItems, the list of items actually stored in the dialog. Then it verifies that every element of names is also an element of actListItems.
@Test
public void testDialogContent()
{
assertEquals( names.length, actListItems.size() );
Arrays.stream( names )
.forEach( n -> assertTrue( actListItems.contains( n ) ) );
}
■ New Test Method: testEmptyList
This method verifies that an empty list is successfully displayed by the ItemSelectionDialog. It also verifies that, if the dialog’s list is empty, its OK button is disabled. The code appears below, followed by annotations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Test public void testEmptyList() { String title = "Empty String Tester"; ItemSelectionDialog dialog = new ItemSelectionDialog( title, new String[0] ); Thread thread = startDialog( dialog ); ComponentFinder finder = new ComponentFinder( true, false, true ); JButton okButton = getButton( finder, "OK" ); JButton cancelButton = getButton( finder, "Cancel" ); assertFalse( okButton.isEnabled() ); SwingUtilities.invokeLater( () -> cancelButton.doClick() ); Utils.join( thread ); assertTrue( selection < 0 ); Predicate<Window> pred = ComponentFinder.getWindowPredicate( title ); finder.setMustBeVisible(false); Window dialogWindow = finder.findWindow( pred ); assertNotNull( dialogWindow ); dialogWindow.dispose(); } |
- Line 4: Declares the title for a new ItemSelectionDialog. This value will be used at line 20 to form a predicate for finding the dialog.
- Lines 5-6: Creates a new ItemSelectionDialog with the given title, and an empty item list (new String[0] creates an array of 0 length). Note that the test application’s window hierarchy now contains two dialogs: this one, and mainDialog, which should not be visible.
- Line 7: Makes the new dialog visible in a dedicated thread.
- Lines 8-9: Instantiates a new ComponentFinder with a configuration that matches only visible dialogs.
- Lines 10-11: Gets the OK and Cancel buttons from the new dialog. Note that mainDialog is not visible, so, given the ComponentFinder configuration, will not be searched.
- Line 12: Verifies that the dialog’s OK button is disabled.
- Line 13: Activates the dialog’s Cancel button.
- Line 14: Waits for the thread running the empty ItemSelectionDialog to expire.
- Line 15: Verifies that the cancelled dialog returned a value less than 0.
- Lines 17-22: Disposes the new dialog, which is no longer needed.
- Lines 17-18: Gets a predicate that will be satisfied by a dialog with the given title.
- Line 19: Reconfigures the ComponentFinder so that it will recognize dialogs that are not visible.
- Line 20: Searches for the dialog with the given title.
- Line 21: Verifies that the dialog was found.
- Line 22: Disposes the dialog.
FileManagerTest
Recall that the original JUnit test for FileManager cannot run unattended; there are two error dialogs posted that you have to be prepared to dismiss by hand. In the revised test we will fix this problem. When we reach a point in a test where we expect an error dialog to be made visible, we can verify that it is, indeed, displayed, then we can find and programmatically activate its OK button.
There are two Windows that we have to be able to interrogate and manipulate in this test; a JFileChooser and a JOptionPane message dialog. The project sandbox contains two programs that demonstrate how to manage these operations, JFileChooserSearch and JOptionPaneSearch.
■ A Little bit about JTextField
A JTextField is a Swing component that can contain a single line of text. Usually this component is intended for the operator to type into, but can be made read-only if desired. The width of the window may be fully determined by the parent component and its layout manager, or you can “suggest” a width by specifying the number of characters it can display. The line of code below creates a JTextField that can display 25 characters at a time. Note that the suggestion doesn’t have to be honored by the parent window. And while the component may only be able to display 25 characters at a time, it can hold many more than 25 characters. JTextField nameField = new JTextField( 25 );
■ JFileChooserSearch
This program demonstrates how to traverse a JDialog´s component hierarchy for the four components that are needed to manage a JFileChooser dialog:
- The Open button. This component is only visible in the dialog if it was started with the showOpenDialog method.
- The Save button. This component is only visible in the dialog if it was started with the showSaveDialog method.
- The Cancel button.
- The text box (type JTextField) which contains the path to the file to save or open.
Here’s a listing of the program, followed by some notes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | public class JFileChooserSearch { private static final ComponentFinder finder = new ComponentFinder(); private static final JFileChooser chooser = new JFileChooser(); private static int count = 0; public static void main(String[] args) throws InterruptedException { printIds( () -> chooser.showSaveDialog( null ) ); printIds( () -> chooser.showOpenDialog( null ) ); System.exit( 0 ); } private static void printIds( Runnable runner ) throws InterruptedException { Thread thread = start( runner ); getJDialog(); JTextField textField = getTextField(); JButton cancelButton = getButton( "Cancel" ); getButton( "Save" ); getButton( "Open" ); System.out.println( "====================" ); SwingUtilities.invokeLater( () -> textField.setText( "Path goes here: " + ++count ) ); Thread.sleep( 3000 ); if ( cancelButton != null ) SwingUtilities.invokeLater( () -> cancelButton.doClick() ); thread.join(); } private static JDialog getJDialog() { Window window = finder.findWindow( w -> true ); String ident = null; JDialog dialog = null; if ( window == null ) ; else if ( !(window instanceof JDialog) ) ident = "??? " + window.getClass().getName(); else { dialog = (JDialog)window; ident = Integer.toHexString( window.hashCode() ) + " "; ident += window.getClass().getName(); } System.out.println( "JDialog: " + ident ); return dialog; } private static JButton getButton( String text ) { Predicate<JComponent> pred = ComponentFinder.getButtonPredicate( text ); String ident = "?????"; JButton button = null; JComponent comp = finder.find( pred ); if ( comp == null ) ident = "null"; else if ( !(comp instanceof JButton) ) ident = comp.getClass().getName(); else { button = (JButton)comp; ident = Integer.toHexString( comp.hashCode() ); } System.out.println( text + ": " + ident ); return button; } private static JTextField getTextField() { JComponent comp = finder.find( c -> (c instanceof JTextField) ); JTextField text = null; String ident = null; if ( comp != null && comp instanceof JTextField ) { text = (JTextField)comp; ident = Integer.toHexString( text.hashCode() ); } System.out.println( "TextField: " + ident ); return text; } private static Thread start( Runnable funk ) throws InterruptedException { Thread thread = new Thread( funk ); thread.start(); Thread.sleep( 500 ); return thread; } } |
- Line 3: The ComponentFinder for searching the window hierarchy. Note that it uses the default configuration; it will find any visible window which is either a JDialog or a JFrame.
- Line 4: The file chooser to display and interrogate.
- Line 6: A flag to display in the discovered text field; incremented each time the dialog is displayed. Note that calling setText causes changes in the dialog’s GUI, so its execution must be scheduled on the EDT.
- Line 10: Calls the printIDs method, which will start the JFileChooser and search its component hierarchy for the four components needed to manage the dialog. Note that with each call it should find either the Open button or the Save button, but not both. The argument indicates that the file chooser should be started with the showSaveDialog method.
- Line 11: Calls the printIDs method, which will start the JFileChoser and search its component hierarchy for the four components needed to manage the dialog. The argument indicates that the file chooser should be started with the showOpenDialog method.
— printIDs method — - Line 18: Starts the dialog in a dedicated thread.
- Line 19: Gets the JDialog component of the file chooser; see line 37, below.
- Line 20: Gets the text field that contains the path to the target file.
- Lines 21-23: Gets the three required button components.
- Line 26-28: Prints a message in the text field, providing visual evidence that the text field was correctly located.
- Line 29: Pauses for three seconds so that you can examine the dialog.
- Lines 30-31: Clicks the Cancel button.
- Line 32: Waits for the dialog to expire.
— getJDialog method — - Line 37: Looks for the JDialog component encapsulated by the JFileChooser. The predicate it passes will be satisfied by any window, but the top-level parameter configuration of the ComponentFinder determines that it finds only visible windows.
- Line 41: Verifies that a component was found.
- Line 43: Verifies that the component is a JDialog.
- Lines 47-49: Prints the hashcode and class name of the component.
- Line 51: Prints a message that will provide feedback about whether or not the dialog was successfully found.
— getButton method — - Lines 57-58: Gets a predicate that will be satisfied if a given JComponent is a JButton and displays the given text.
- Line 62: Attempts to locate the button.
- Line 63: Verifies that a component was found.
- Line 65: Verifies that the component is a JButton.
- Lines 67 -71: Prints the hashcode of the JButton.
— getTextField method — - Line 79: Finds the first JTextField in the window hierarchy.
- Lines 82-87: Validates the component returned by find, and prints an identifying message to stdout.
- Lines 91-99: Starts the given Runnable in a dedicated thread, waits for the thread to initialize and execute, and returns the created thread.
■ JOptionPaneSearch
When managing a JOptionPane message dialog, the components of interest are the JLabel that displays the message, and the OK button that dismisses the dialog. The program that demonstrates how to do this is not substantially different in essence from JFileChooserSearch, so I won’t try to discuss it in exhaustive detail. The only new feature of JOptionPaneSearch is that it searches for a JLabel with text that contains a given string. The method that returns the text of a JLabel is getText, and it’s possible for this method to return null; for that reason I wrote a helper method to assist in testing the text of a label against a given string. Here’s the helper method, and the method that searches for the label. It’s followed by some notes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | private static boolean contains( String contained, String container ) { boolean result = false; if ( contained != null && container != null ) result = container.contains( contained ); return result; } private static JLabel getLabel( String containedText ) { Predicate<JComponent> notNull = jc -> jc != null; Predicate<JComponent> isLabel = jc -> jc instanceof JLabel; Predicate<JComponent> contains = jc -> contains( containedText, ((JLabel)jc).getText() ); Predicate<JComponent> pred = notNull.and( isLabel ).and( contains ); JComponent comp = finder.find( pred ); JLabel label = null; if ( comp != null && comp instanceof JLabel ) label = (JLabel)comp; if ( label != null ) System.out.println( "Label: " + label.getText() ); return label; } |
- Line 11: Declares a Predicate asserting that a given JComponent is not null.
- Line 12: Declares a Predicate asserting that a given JComponent is a JLabel.
- Lines 13-14: Declares a Predicate asserting that the text of a JLabel contains a given string.
- Lines 15-16: Declares a Predicate asserting that a JComponent is not null, AND is type JLabel, AND has text that contains the given string.
- Line 17: Searches for a component that satisfies pred.
- Lines 18-23: Validates the result of the above search, and prints descriptive information to stdout.
FileManagerTest: Revised Test Methods
The methods that need to be revised for the FileManager JUnit test are any of those that use Robot, or display an error message. They are:
- testSaveOpenEquationApprove()
- testSaveOpenEquationCancel()
- testSaveWithIOError() (note that, in the original test, this method was badly name saveWithIOError)
- testOpenWithIOError() (note that, in the original test, this method was badly name openWithIOError)
■ Test Class State, Helper Methods
To facilitate revisions I have added five instance variables to the unit test to encapsulate the relevant components of the JFileChooser dialog:
- jDialog: encapsulates the JDialog component of the file chooser.
- saveButton: encapsulates the file chooser’s Save button. Note, by the way that this value will be null whenever the dialog is posted for an open operation.
- openButton: encapsulates the file chooser’s Open button. Note, by the way that this value will be null whenever the dialog is opened for a save operation.
- cancelButton: encapsulates the file chooser’s Cancel button.
- pathTextField: encapsulates the file chooser’s JTextField.
The above variables are each set to null in the test class’s @BeforeEach method. They are initialized in the revised startDialog method, which now looks like this:
private Thread startDialog( Runnable runner )
{
Thread thread = new Thread( runner );
thread.start();
Utils.pause( 500 );
jDialog = getJDialog();
saveButton = getButton( "Save" );
openButton = getButton( "Open" );
cancelButton = getButton( "Cancel" );
pathTextField = getTextField();
return thread;
}
The new helper methods referenced above, getJDialog, getButton and getTextField are not substantially different from their counterparts in the JFileChooserSearch program discussed above. If you want to see the code you can find it in the GitHub repository.
We have another new helper method, expectErrorDialog. This method is invoked in a dedicated thread immediately before we execute an operation which we expect to open a message dialog, for example:
Thread thread = new Thread( () -> expectErrorDialog() );
thread.start();
Equation equation = FileManager.open( "nobodyhome" );
This is about to get a little complicated. One of the complications is that expectErrorDialog has to wait in a loop until the error dialog becomes visible. One way to do that is like this:
// Careful! There's a problem with this logic
do
{
window = finder.findWindow( w -> true );
} while ( window == null );
// is window the error dialog or the file chooser dialog?
The line of code window = finder.FindWindow() will return the first visible window it sees, but that window might be the file chooser dialog. How do we distinguish between the file chooser dialog and the error dialog we’re waiting for? One distinguishing characteristic is that the error dialog has an OK button, but the file chooser dialog does not. To take advantage of this we have a helper method that looks like this:
// Try to find and return an OK button. If not found, return null.
private JButton getErrorDialogOKButton()
{
ComponentFinder finder =
new ComponentFinder( true, false, true );
Predicate<JComponent> pred =
ComponentFinder.getButtonPredicate( "OK" );
JComponent comp = finder.find( pred );
JButton button = null;
if ( comp instanceof JButton )
button = (JButton)comp;
return button;
}
Now our main loop looks like this:
// This is much improved from before, but we still have
// another complication to deal with.
do
{
button = getErrorDialogOKButton();
} while ( button == null );
The second complication arises from a timing issue. To begin with, we don’t want to sit in a loop saying gimme gimme gimme until I’ve got what I’ve come for. (For the record, this is called a spin loop, and it’s a very, very selfish programming technique.) Instead we want to pause for a bit between gimmes, giving other processes and threads a halfway decent chance to run:
// Almost have a completed wait strategy now.
do
{
pause( some-number-of-milliseconds );
button = getErrorDialogOKButton();
} while ( button == null );
Finally we have to be prepared for our test to fail; if it does the error dialog will never appear, and our wait loop could run forever. So after some time interval we have to stop waiting and declare that our test failed.
// Final strategy.
do
{
pause( some-number-of-milliseconds );
button = getErrorDialogOKButton();
} while ( button == null && total-wait-time < max );
assertNotNull( button );
To implement this strategy we have two new parameters declared as class variables. The first says how long we’re going to wait before trying to find the OK button, and the second says how long we’re going to wait before we decide we’ve got a test failure. Once we have these two parameters we can finalize our expectErrorDialog method. In the following code System.currentTimeMillis is a method that gives us the current time in milliseconds, so startMillis = System.currentTimeMillis() gives us the time we started our loop, and System.currentTimeMillis() – startMillis gives us the total about of time our loop has been running. Note that there’s one more complication…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // ... private static final long expDialogPauseMillis = 250; private static final long expDialogMaxWaitMillis = 2000; // ... private void expectErrorDialog() { long startMillis = System.currentTimeMillis(); long totalMillis = 0; JButton okButton = null; do { Utils.pause( expDialogPauseMillis ); okButton = getErrorDialogOKButton(); totalMillis = System.currentTimeMillis() - startMillis; } while ( okButton == null && totalMillis < expDialogMaxWaitMillis ); assertNotNull( okButton,"timeout waiting for error dialog" ); JButton button = okButton; SwingUtilities.invokeLater( () -> button.doClick() ); } |
The final complication has to do with the call to invokeLater. Recall that:
- () -> button.doClick() is a lambda.
- A lambda is a shortcut for an anonymous class.
- Anonymous classes can only refer to local variables that are final or effectively final.
We see (line 9) that okButton is a local variable, which (line 13) is not final, so cannot be used in the lambda (line 19). The solution is to copy the button to a local variable that is effectively final (line 18) and then use that variable in the lambda.
◼ Revised test method testOpenWithIOError
◼ Revised test method testSaveWithIOError
Now that we’ve got our expectErrorDialog method we can put it to work in the two methods that generate error dialogs. The first one, testOpenWithIOError is easier to read than the other, testSaveWithIOError, but they both employ the same wait-for-and-dismiss-error-dialog logic. Here’s the code for testOpenWithIOError ; the code for saveWithIOError can be found in the GitHub repository.
@Test
public void testOpenWithIOError()
{
Thread thread = new Thread( () -> expectErrorDialog() );
thread.start();
Equation equation = FileManager.open( "nobodyhome" );
Utils.join( thread );
assertNull( equation );
}
◼ Revised test method testSaveOpenEquationApprove()
That leaves us with the test methods that have to be revised to remove the Robot logic. The first is the testSaveOpenEquationApprove method. For starters I split this method into two test methods, testSaveEquationApprove and testOpenEquationApprove. The Robot logic was deleted, and the following code substituted:
- In the save test method:
SwingUtilities.invokeLater(() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> saveButton.doClick() ); - In the open test method:
SwingUtilities.invokeLater(() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> openButton.doClick() );
The code for these two methods looks like this:
@Test
public void testOpenEquationApprove()
{
FileManager.save( testFile, testEquation );
assertTrue( testFile.exists() );
Thread thread = startDialog( () -> execOpenCommand() );
SwingUtilities.invokeLater(
() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> openButton.doClick() );
Utils.join( thread );
assertEqualsDefault( openEquation );
}
@Test
public void testSaveEquationApprove()
{
assertFalse( testFile.exists() );
Thread thread = startDialog( () -> execSaveCommand() );
SwingUtilities.invokeLater(
() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> saveButton.doClick() );
Utils.join( thread );
assertTrue( testFile.exists() );
}
◼ Revised test method testSaveOpenEquationCancel()
As with the previous test method, I have split this method into two, testSaveEquationCancel and testOpenEquationCancel. The Robot logic was deleted, and the following code substituted:
- In the save test method:
SwingUtilities.invokeLater(() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> cancelButton.doClick() ); - In the open test method:
SwingUtilities.invokeLater(() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> cancelButton.doClick() );
The code for these methods is similar to, and, if anything, simpler than their …Approve cousins.
@Test
public void testSaveEquationCancel()
{
Thread thread = startDialog( () -> execSaveCommand() );
SwingUtilities.invokeLater(
() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> cancelButton.doClick() );
Utils.join( thread );
assertFalse( testFile.exists() );
}
@Test
public void testOpenEquationCancel()
throws AWTException, InterruptedException
{
FileManager.save( testFilePath, testEquation );
Thread thread = startDialog( () -> execOpenCommand() );
SwingUtilities.invokeLater(
() -> pathTextField.setText( testFilePath ) );
SwingUtilities.invokeLater( () -> cancelButton.doClick() );
thread.join();
assertNull( openEquation );
}
EquationMapTest
Once again our goal in revising this test is to remove the Robot code, substituting direct manipulation of the GUI. That includes:
- testParseEquationFilesSelectFileApprove which displays a file chooser dialog and invites the user to select a file.
- testParseEquationFilesSelectDirApprove which displays a file chooser dialog and expects the user to select a directory.
- testParseEquationFilesCancel displays a file chooser, and expects the operator to select Cancel
- testGetEquationOK which displays the equation selection dialog and expects the operator so select OK.
- testGetEquationCancel which displays the equation selection dialog and expects the operator so select Cancel.
◼ Revised test method testParseEquationFilesSelectFileApprove ()
The method under test, parseEquationFiles, displays the file chooser dialog and allows the operator to select either a single file, or a directory. This test calls parseEquationFiles in a dedicated thread, enters the path to a single file in the file chooser´s JTextField, and clicks Open. Then it verifies that the target file was correctly opened.
@Test
void testParseEquationFilesSelectFileApprove()
{
Thread thread = startDialog( () ->
EquationMap.parseEquationFiles()
);
SwingUtilities.invokeLater(
() -> pathTextField.setText( testFiles[0].getAbsolutePath() ) );
SwingUtilities.invokeLater( () -> openButton.doClick() );
Utils.join( thread );
Map<String,Equation> map = EquationMap.getEquationMap();
assertEquals( 1, map.size() );
verifyEquation( testEquations[0], map.get( testNames[0] ) );
}
◼ Revised test method testParseEquationFilesSelectDirApprove ()
In this case, parseEquationFiles is again under test. But this time the path to a directory is entered into the file chooser’s text field and the Open button is clicked. Afterward we verify that every equation file in the selected directory is loaded.
@Test
void testParseEquationFilesSelectDirApprove()
{
Thread thread = startDialog( () ->
EquationMap.parseEquationFiles()
);
String tempPath = tempDir.getAbsolutePath();
SwingUtilities.invokeLater( () -> pathTextField.setText( tempPath ) );
SwingUtilities.invokeLater( () -> openButton.doClick() );
Utils.join( thread );
Arrays.stream( testNames )
.forEach( n -> verifyEquation(
testMap.get( n ), EquationMap.getEquation( n )
));
}
◼ Revised test method testParseEquationFilesCancel()
One more test for parseEquationFiles. This time after posting the dialog, the Cancel button is clicked. Then we verify that no equation files have been loaded. The complete code can be found in the GitHub repository.
◼ Revised test method testGetEquationOK()
◼ Revised test method testGetEquationCancel()
These tests call the getEquation method with no arguments, causing the method to post an ItemSelectionDialog so the operator can select an equation. The method is called in a dedicated thread, then either the OK or Cancel button is clicked. If OK is selected, we verify that the correct equation is returned, otherwise we verify that no equation is returned. The code for testGetEquationOK follows. The code for testGetEquationCancel can be found in the GitHub repository.
@Test
void testGetEquationOK()
{
EquationMap.parseEquationFiles( tempDir );
List<Equation> equations = Arrays.asList( testEquations );
equations.sort( (i1, i2) -> i1.getName().compareTo( i2.getName() ) );
Equation expEquation = equations.get( 2 );
Thread thread = startDialog( () ->
selectedEquation = EquationMap.getEquation()
);
SwingUtilities.invokeLater( () -> jList.setSelectedIndex( 2 ) );
SwingUtilities.invokeLater( () -> okButton.doClick() );
Utils.join( thread );
verifyEquation( expEquation, selectedEquation );
}
Summary
This concludes lesson 14, in which we developed a facility to interrogate an application’s GUI, allowing us to locate and manipulate any component in its window hierarchy. This page was devoted to revising our JUnit tests for the ItemSelectionDialog, FileManager and EquationMap classes; with one exception, all references to Robot were replaced with direct manipulation of GUI components. The exception was in the ItemSelectionDialog class, where we had to verify that pressing enter activated the OK button, and pressing escape activated the Cancel button.
In our next lesson we will begin assembling the GUI components that can be used to control our Cartesian Plane application in detail.