On the previous page of this lesson, we implemented a facility to decompose an appliation’s GUI, allowing us to identify and manipulate all of its constituent components. On this page we’ll develop an automated unit test for that facility. See also:
- Page 1: Cartesian Plane Lesson 14: GUI Analysis
- Page 3: Cartesian Plane Lesson 14: GUI Testing: Revised JUnit Tests
GitHub repository: Cartesian Plane Part 14
Previous lesson: Cartesian Plane Lesson 13: More Extensions
Testing the ComponentFinder Utility
The ComponentFinderTest Class
This is going to be a fairly big class. To support testing we’re going to need to be able to create dialogs and frames, and configure them in various ways (visible, not visible, disposed, not disposed). To that end we are eventually going to wind up with nested classes for creating test JDialogs and test JFrames. For convenience, we’ll have a third nested class to encapsulate a content pane for each of the above. It turns out we’re even going to have a nested interface. But we’ll worry about the details of nested classes and interfaces later. Let’s start with categorizing all the things we’ll have to test.
- Constructors
- Getters, setters
- Boolean getters, setters (setCanBeFrame, isMustBeVisible, etc.)
- Top-level filter getter, setter
- Miscellaneous convenience methods
- disposeAll,
- getButtonPredicate
- getWindowPredicate
- Searching methods
- Methods dependent on top-level search parameters (see below for additional detail)
- Window findWindow( Predicate<Window> pred )
- JComponent find( Predicate<JComponent> pred )
- Methods not dependent on top-level search parameters
- JComponent find( Window window, Predicate<JComponent> pred )
- JComponent find( JComponent container, Predicate<JComponent> pred )
- Methods dependent on top-level search parameters (see below for additional detail)
Test Class Configuration
Under normal circumstances, the order in which you run tests should not be relevant to any one test. You should be able to run the tests in any order, and, in fact, you should be able to run any one test stand-alone. But there’s a problem with our test for disposeAll. As mentioned above, we’re going to start out with a set of dialogs and frames, some of which will be visible, some of which will be disposed (i.e., not displayable). But as soon as we call disposeAll every one of our frames and dialogs will be reconfigured. There are several ways we could solve this problem, but the way we’re going to do it is to force testDisposeAll to be executed last.
Note: for details about ordering of test method execution, see the JUnit 5 Documentation:
We’ll start by declaring our test class with the annotation @TestMethodOrder. This must be followed with (in parentheses) a description of the particular order algorithm to use. MethodOrderer.OrderAnnotation.class tells JUnit that we are going to assign specific ordinals to our test methods: @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ComponentFinderTest
Ordinals are assigned with the @Order annotation preceding the @Test annotation of a test method:
@Order( 5 )
@Test
public void testSomething()
Not every test method has to have a declared ordinal. If we don’t give a method an explicit ordinal, JUnit will assign it a default of Integer.MAX_VALUE / 2. So we will assign an ordinal of Integer.MAX_VALUE to the testDisposeAll method, and declare nothing for all the other test methods:
@Test
public void testGetButtonPredicate()
{
// ...
}
@Order( Integer.MAX_VALUE )
@Test
public void testDisposeAll()
{
// ...
}
Testing the Constructors
We have two constructors to test. The ComponentFinder(boolean,boolean,boolean) constructor has to be tested several times, with different configurations of the parameters. We’ll put all our constructor tests in one method, and we’ll have a helper method to reduce the typing we have to do for the non-default constructor. The helper method looks like this:
private void
testCtor( boolean cbDialog, boolean cbFrame, boolean mbVisible )
{
ComponentFinder finder =
new ComponentFinder( cbDialog, cbFrame, mbVisible );
assertEquals( cbDialog, finder.isCanBeDialog() );
assertEquals( cbFrame, finder.isCanBeFrame() );
assertEquals( mbVisible, finder.isMustBeVisible() );
assertTrue( finder.isMustBeDisplayable() );
}
And he JUnit test method looks like this, beginning with a test for the default constructor.
@Test
public void testCtors()
{
ComponentFinder finder = new ComponentFinder();
assertTrue( finder.isCanBeDialog() );
assertTrue( finder.isCanBeFrame() );
assertTrue( finder.isMustBeVisible() );
assertTrue( finder.isMustBeDisplayable() );
testCtor( true, true, true );
testCtor( false, true, true );
testCtor( true, false, true );
testCtor( true, true, false );
}
Testing the Setters and Getters
Once again, we’re going to put all the testing for the four pairs of Boolean setters/getters in a single method, and put most of the work into a helper method. Here’s the code, beginning with the helper method. The first two parameters are functional interfaces corresponding to a specific getter/setter pair, and the third parameter is a comment that we can use in the method’s assertions. (Recall that, if an assertion fails, the comment will be included in the subsequent diagnostic.)
private void testBooleanAccessors(
Consumer<Boolean> setter,
BooleanSupplier getter,
String remark
)
{
setter.accept( false );
assertFalse( getter.getAsBoolean(), remark );
setter.accept( true );
assertTrue( getter.getAsBoolean(), remark );
}
@Test
public void testBooleanAccessors()
{
ComponentFinder finder = new ComponentFinder();
testBooleanAccessors(
finder::setCanBeDialog,
finder::isCanBeDialog,
"can-be-dialog"
);
testBooleanAccessors(
finder::setCanBeFrame,
finder::isCanBeFrame,
"can-be-frame"
);
testBooleanAccessors(
finder::setMustBeVisible,
finder::isMustBeVisible,
"must-be-visible"
);
testBooleanAccessors(
finder::setMustBeDisplayable,
finder::isMustBeDisplayable,
"must-be-displayable"
);
}
Testing the setter/getter for the top-level filter is fairly trivial. (Note that the “trivial” comment refers to making sure the setter and getter work properly; later we’re going to have to make sure that, if the top-level filter is set, the findWindow and find methods use it correctly.)
@Test
public void testTopWindowFilterAccessors()
{
ComponentFinder finder = new ComponentFinder();
Predicate<Window> filter = w -> (w instanceof JDialog);
finder.setTopWindowFilter( filter );
assertEquals( filter, finder.getTopWindowFilter() );
}
Testing the Convenience Methods
◼ The getButtonPredicate Method
The predicate returned by this method consists of a compound logical expression: the component is a JButton AND it has the given text. Our test, then, consists of three parts:
- The predicate passes if the given component has both properties.
- The predicate fails if the give component is not a JButton.
- The predicate fails if the give component is a JButton, but does not have the given text.
Our test method begins by declaring three components, matching the above three configurations.
@Test
public void testGetButtonPredicate()
{
String buttonLabel = "Button Label";
String notButtonLabel = "Not Button Label";
JButton expButton = new JButton( buttonLabel );
JButton notExpButton = new JButton( notButtonLabel );
JLabel notButton = new JLabel( buttonLabel );
Predicate<JComponent> buttonPred =
ComponentFinder.getButtonPredicate( buttonLabel );
assertFalse( buttonPred.test( notExpButton ) );
assertFalse( buttonPred.test( notButton ) );
assertTrue( buttonPred.test( expButton ) );
}
◼ The getWindowPredicate Method
The predicate returned by this method asserts that:
(a given window is a JDialog AND has a given title)
-- OR --
(a given window is a JFrame AND has a given title)
To test it we make four windows with unique titles: a JDialog titled Dialog A and a JDialog titled Dialog B; and a JFrame titled Frame A, and a JFrame with with the title Frame B. Then:
- Get a predicate asserting that a top-level window has title Dialog A; the first JDialog should pass, and the other three dialogs should fail.
- Get a predicate asserting that a top-level window has title Frame A; the first JFrame should pass, and the other three dialogs should fail.
This is what the test method looks like:
@Test
public void testGetWindowPredicate()
{
String dialogTitle = "Dialog Title";
String notDialogTitle = "Not Dialog Title";
JDialog dialogSuccess = new JDialog( (Window)null, dialogTitle );
JDialog dialogFail = new JDialog( (Window)null, notDialogTitle );
String frameTitle = "Frame Title";
String notFrameTitle = "Not Frame Title";
JFrame frameSuccess = new JFrame( frameTitle );
JFrame frameFail = new JFrame( notFrameTitle );
Window notDialogOrFrame = new Window( (Window)null );
Predicate<Window> dialogPred =
ComponentFinder.getWindowPredicate( dialogTitle );
Predicate<Window> framePred =
ComponentFinder.getWindowPredicate( frameTitle );
assertFalse( dialogPred.test( dialogFail ) );
assertFalse( dialogPred.test( frameSuccess ) );
assertFalse( dialogPred.test( frameFail ) );
assertFalse( dialogPred.test( notDialogOrFrame ) );
assertTrue( dialogPred.test( dialogSuccess ) );
assertFalse( framePred.test( frameFail ) );
assertFalse( framePred.test( dialogSuccess ) );
assertFalse( framePred.test( dialogFail ) );
assertFalse( framePred.test( notDialogOrFrame ) );
assertTrue( framePred.test( frameSuccess ) );
}
◼ The disposeAll Method
To test this method we’ll start by creating two displayable dialogs. Then, as a sanity check, we’ll verify that Java’s Window array contains at least two non-disposed dialogs. Then we’ll dispose the dialogs, via the disposeAll method, and verify that all windows are now non-displayable. Note also, as discussed above, this test method has an ordinal value of Integer.MAX_VALUE.
@Order( Integer.MAX_VALUE )
@Test
public void testDisposeAll()
{
// In case we're running this test stand-alone, make
// sure that there are windows to dispose.
new JDialog().pack();
new JDialog().pack();
// Sanity check: there must be at least two windows
// that have not yet been disposed.
long count = Arrays.stream( Window.getWindows() )
.filter( Window::isDisplayable )
.count();
assertTrue( count > 1 );
ComponentFinder.disposeAll();
// Verify all windows have now been disposed
count = Arrays.stream( Window.getWindows() )
.filter( Window::isDisplayable )
.count();
assertEquals( count, 0 );
}
Testing the Search Methods
◼ Testing Infrastructure
To test our search methods we’re going to need windows, dialogs and frames, some of which are visible, some of which are not visible, and some of which are not displayable. Some of the components within the windows will need to be reasonably deeply nested, so we can prove that our recursive search algorithm works correctly. I propose that we have a nested class corresponding to a JDialog and another corresponding to a JFrame. All such dialogs will need content panes, which we can construct from yet a third nested class. The content pane will have two nested JPanels, and each panel will have two nested buttons, to be used as the target of many or our search tests…
.. And one more thing. Think back to the code we had to add to the ComponentFinder class, which had to locate a content pane in what might be a JDialog or might be a JFrame:
JComponent getContentPane( Window window )
contentPane = null
if window is a JDialog
contentPane = ((JDialog)window).getContentPane
else if window is a JFrame
contentPane = ((JFrame)window).getContentPane
else
// ...
We’d like to have a way to avoid this kind of miserable code in our testing. Also, since we sometimes want to run tests on dialogs and frames at the same time, it would be nice if we could have dialogs and frames living comfortably in the same lists or arrays.
◼ The TestWindow Interface
We can have all of the above by encapsulating everything we think should be common to dialogs and frames in an interface, then have our dialog-test class and frame-test class implement the interface. Here’s the interface I came up with, followed by some notes. Note that the declaration of the interface is nested directly inside our test class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private interface TestWindow { String getTitle(); List<String> getLabels(); JPanel getContentPane(); Window getWindow(); } // ... private static class TestDialog implements TestWindow { // ... } // ... private static class TestFrame implements TestWindow { // ... } |
- Line 3: This method allows us to get the title of the dialog or frame. Assuming titles are unique across test windows, this will allow us to search for specific windows, and verify that they were found.
- Line 4: This method returns all of the labels from all of the buttons nested within a particular test window. Looking ahead a bit, we are going to ensure that every button has a unique label, for example VisibleFrameOK and VisibleFrameCancel will be two of the button labels on the visible test frame. VisibleDialogOK and VisibleDialogCancel will the the labels on two of the buttons in the visible test dialog. Likewise there will be a disposed dialog and a disposed frame with button labels like DisposedDialogOK and DisposedFrameOK.
- Line 5: This method will return the content pane of the encapsulated window.
- Line 6: This method will return the Window (either a JDialog or a JFrame) of the encapsulated window.
- Lines 9 and 14: Because our test windows implement TestWindow we can create lists of both dialog and frame variables for testing:
private static TestDialog visibleDialog;
private static TestDialog notVisibleDialog;
private static TestFrame visibleFrame;
private static TestFrame notVisibleFrame;
// ...
@Test
public someSearchTest()
{
List<TestWindow> shouldFind =
List.of(
visibleDialog,
notVisibleDialog,
visibleFrame,
notVisibleFrame
);
// ...
}

◼ The TestContentPane Nested Class
Both the dialog and the frame test classes will use the same content pane. The content pane contains two JPanels, and each JPanel contains two buttons. There will be six dialogs total, and all the buttons will have unique labels, allowing us to search for a specific button from the top of the window hierarchy to the bottom.
The figure to the right depicts the physical layout of one of our test windows. Recall that the layout of a component is controlled by a layout manager. The content pane and its two nested panels use a GridLayout. This layout manager divides an area into rows and columns, each cell of which occupies the same amount of space. The content pane uses a GridLayout( 2, 1 ), dividing it’s area into two rows of one column each. The nested panels use a GridLayout( 1, 2 ), dividing each into a single row with two columns.
The code for the nested class is 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | private static final String abortLabel = "Abort"; private static final String cancelLabel = "Cancel"; private static final String exitLabel = "Exit"; private static final String okLabel = "OK"; // ... public static class TestContentPane extends JPanel { private final JPanel panel1; private final JPanel panel2; private final JButton okButton; private final JButton cancelButton; private final JButton exitButton; private final JButton abortButton; public TestContentPane( String prefix ) { super( new GridLayout( 2, 1 ) ); panel1 = new JPanel( new GridLayout( 1, 2 ) ); this.add( panel1 ); okButton = new JButton( prefix + okLabel ); cancelButton = new JButton( prefix + cancelLabel ); panel1.add( okButton ); panel1.add( cancelButton ); panel2 = new JPanel( new GridLayout( 1, 2 ) ); this.add( panel2 ); exitButton = new JButton( prefix + exitLabel ); abortButton = new JButton( prefix + abortLabel); panel2.add( exitButton ); panel2.add( abortButton ); } public List<String> getLabels() { List<String> list = List.of( okButton.getText(), cancelButton.getText(), exitButton.getText(), abortButton.getText() ); return list; } } |
- Lines 1-4: Base labels for use on buttons. During button instantiation, these labels will be combined with prefixes that make them unique throughout the window hierarchy.
- Line 6: Nested class declaration. Note that this class extends JPanel.
- Lines 8-13: Instance variables representing all of the content panes nested components.
- Line 14: Constructor. The prefix parameter is a string that is added to each button label to ensure its uniqueness.
- Line 17: Invokes the constructor in the superclass. The argument it’s passing is the layout manager that the content pane will use to layout its nested components (see TestContentPane, above).
- Line 19: Instantiates the JPanel that constitutes the first of the content pane’s nested components; it’s using the JPanel constructor that sets the JPanel’s layout manager (see TestContentPane, above).
- Line 20: Adds the above panel to its list of nested components.
- Lines 21-22: Creates the two buttons that will go in the first panel. Note that, for labels, it is using prefix + okLabel and prefix + cancelLabel.
- Lines 23-24: Adds the buttons to the first panel’s list of nested components.
- Lines 26-27: Creates the second of the content pane’s nested components.
- Lines 28-31: Creates the two buttons that go in the second panel, and adds them to the panel.
- Lines 34 – 43: Method to return a list of the constituent buttons’ labels. These labels will subsequently be used in tests such as “search all visible dialogs for a JButton with the label visibleDialogOK.”
◼ The TestDialog Nested Class
This class encapsulates a JDialog to be used for testing. Note that it implements TestWindow. The annotated code follows.
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 | private static final String dialogID = "Dialog"; private static final String titleLabel = "Title"; // ... private static class TestDialog implements TestWindow { private final JDialog dialog; private final TestContentPane contentPane; private final String title; public TestDialog( String prefix ) { String thisID = prefix + dialogID; title = thisID + titleLabel; dialog = new JDialog( (Window)null, title ); dialog.setModal( false ); contentPane = new TestContentPane( thisID ); dialog.setContentPane( contentPane ); dialog.pack(); } public void show() { dialog.setVisible( true ); } public void dispose() { dialog.dispose(); } @Override public JPanel getContentPane() { return contentPane; } @Override public String getTitle() { return title; } @Override public Window getWindow() { return dialog; } @Override public List<String> getLabels() { return contentPane.getLabels(); } } |
- Line 1: This is a string to be added to each test dialog title. It allows us to determine, from its title, whether a window is a dialog or a frame.
- Line 2: Base window title. This will later be combined with dialogID and a prefix to give each top-level window a unique title.
- Line 4: Nested class declaration. Note that this class implements TestWindow.
- Lines 6-8: Instance variable for storage of individual components of the test dialog.
- Line 10: Constructor. The prefix parameter is a string intended to be added to the dialog’s title, thereby giving it a title that’s unique among all top-level windows in the window hierarchy.
- Lines 12-13: Formulates a title for the dialog from the titleLabel, dialogID and prefix values.
- Line 14: Instantiates a JDialog with a null owner and the above formulated title. (By the way, JDialog has three constructors that match the parameters null, title; they are JDialog(Dialog,String), JDialog(Frame,String) and JDialog(Window,String). Since we’re passing null for the owner, the compiler can’t decide which of the three constructors to use, so we help it out by telling it to treat null as type Window.)
- Line 15: Makes the dialog non-modal.
- Lines 16-17: Creates and sets the dialog’s content pane.
- Line 19: Initiates the window layout process.
- Lines 22-25: Method to make the dialog visible.
- Lines 27-30: Method to dispose the dialog.
- Lines 32-36: Method to return the test dialog’s content pane. Overrides the abstract method declared in the TestWindow interface.
- Lines 38-42: Method to return the test dialog’s title. Overrides the abstract method declared in the TestWindow interface.
- Lines 44-48: Method to return the test dialog’s encapsulated JDialog. Overrides the abstract method declared in the TestWindow interface.
- Lines 50-54: Method to return the labels of the test dialog’s constituent buttons. Overrides the abstract method declared in the TestWindow interface.
◼ The TestFrame Nested Class
This class is nearly identical to TestDialog. Let’s look at an abbreviated listing of the code, then I’ll point out the primary differences.
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 | private static final String frameID = "Frame"; private static final String titleLabel = "Title"; // ... private static class TestFrame implements TestWindow { private final JFrame frame; private final TestContentPane contentPane; private final String title; public TestFrame( String prefix ) { String thisID = prefix + frameID; title = thisID + titleLabel; frame = new JFrame( title ); contentPane = new TestContentPane( thisID ); frame.setContentPane( contentPane ); } public void start( boolean makeVisible ) { try { SwingUtilities.invokeAndWait( () -> { frame.pack(); }); frame.setVisible( makeVisible ); } catch ( InterruptedException | InvocationTargetException exc ) { exc.printStackTrace(); System.exit( 1 ); } } public void dispose(){ ... } @Override public JPanel getContentPane(){ ... } @Override public String getTitle(){ ... } @Override public Window getWindow(){ ... } @Override public List<String> getLabels(){ ... } } |
- Line 6: TestFrame encapsulates a JFrame instead of a JDialog.
- Line 12: TestFrame uses its title to identify itself as a frame.
- Line 17: TestFrame doesn’t initiate component layout (it doesn’t call pack()) in its constructor; it does that in its start() method.
- Lines 19-33: Instead of a show method, TestFrame has a start method.
- Lines 23-25: Initiates frame layout using the SwingUtilities class. We’ve seen this process in previous lessons when we used SwingUtilities.invokeLater(). The main difference between invokeLater() and invokeAndWait() is that invokeLater() starts the process in a separate thread, then immediately returns to the caller; invokeAndWait() starts the process in a separate thread, then waits for the process to complete before returning. Another difference is that invokeAndWait() might throw a checked exception, so we have to call it in a try/catch block.
◼ Test Class State Variables
Virtually all of our test data is going to be bound in variables, and initialized before the first test is executed. The initialization will be done in a @BeforeAll metthod. @BeforeAll methods are required to be static, consequently most of our variables will be class variables. Here’s an annotated list.
1 2 3 4 5 6 7 8 9 10 11 12 13 | private static final String frameID = "Frame"; private static final String dialogID = "Dialog"; private static final String disposedPrefix = "Disposed"; private static final String notVisiblePrefix = "NotVisible"; private static final String visiblePrefix = "Visible"; private static TestDialog visibleDialog; private static TestDialog notVisibleDialog; private static TestDialog disposedDialog; private static TestFrame visibleFrame; private static TestFrame notVisibleFrame; private static TestFrame disposedFrame; |
- Line 1: String which will appear in the title of every test frame, and on the labels of all the frame’s nested buttons.
- Line 2: String which will appear in the title of every test dialog, and on the labels of all the dialog’s nested buttons.
- Line 3: String which will appear in the title of every disposed window, and on the labels of all the window’s nested buttons.
- Line 4: String which will appear in the title of every non-visible window, and on the labels of all the window’s nested buttons.
- Line 5: String which will appear in the title of every visible window, and on the labels of all the window’s nested buttons.
- Line 7: This variable will reference a TestDialog that is visible on the screen. It will be used in conjunction with the top-level search parameters can-be-dialog and must-be-visible.
- Line 8: This variable will reference a TestDialog that is ready to be displayed, but has not been made visible. It will be used in conjunction with the top-level search parameters can-be-dialog and must-be-visible.
- Line 9: This variable will reference a TestDialog that has been fully created and readied for display but then disposed. It will be used in conjunction with the top-level search parameters can-be-dialog and must-be-displayable.
- Lines 11-13: These variables are directly analogous to the previous three, but reference visble TestFrames, non-visible TestFrames and disposed TestFrames.
◼ Test Data Initialization
Following is the @BeforeAll method that perform test data initialization, along with some notes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @BeforeAll public static void beforeAll() throws Exception { visibleDialog = new TestDialog( visiblePrefix ); visibleDialog.show(); notVisibleDialog = new TestDialog( notVisiblePrefix ); disposedDialog = new TestDialog( disposedPrefix ); disposedDialog.dispose(); visibleFrame = new TestFrame( visiblePrefix ); visibleFrame.start( true ); notVisibleFrame = new TestFrame( notVisiblePrefix ); notVisibleFrame.start( false ); disposedFrame = new TestFrame( disposedPrefix ); disposedFrame.start( false ); disposedFrame.dispose(); } |
- Lines 4-5: Creates a test dialog and makes it visible. The window title, and the labels on all its constituent buttons, will begin with “VisibleDialog”.
- Line 6: Creates a test dialog (which will never be made visible). The window title, and the labels on all its constituent buttons, will begin with “NotVisibleDialog”.
- Line 7: Creates a test dialog, then disposes it. The window title, and the labels on all its constituent buttons, will begin with “DisposedDialog”.
- Lines 10-11: Creates a test frame and makes it visible. The window title, and the labels on all its constituent buttons, will begin with “VisibleFrame”.
- Line 12: Creates a test frame (which will never be made visible). The window title, and the labels on all its constituent buttons, will begin with “NotVisibleFrame”.
- Line 13: Completes the process that will make the previous frame displayable (but not visible).
- Line 14: Creates a test frame. The window title, and the labels on all its constituent buttons, will begin with “DisposedFrame”.
- Line 15: Makes the previous frame displayable (but not visible).
- Line 16: Disposes the previous frame.
◼ Testing the Top-level Window Methods
Now we have to decide how we’re going to test the methods that start searching at the top of the window hierarchy, findWindow(Predicate<Window>) and find(Predicate<JComponent>). These will have to include tests that include parameters that search just dialogs, just frames and both dialogs and frames. Some of them have to exercise the must-be-visible parameter, and some must exercise the must-be-displayable parameter. Every possible combination of the first three parameters gives as many as seven different tests, and throwing the fourth gets us twice that (you can’t count any test in which visible would be true and displayable false). But I really don’t think we need that many tests. I propose eight test configurations for each top-level method. They’re listed in the following table.
Window
findWindow( Predicate )
JComponent
find( Predicate
)
can be dialog | can be frame | must be visible | must be displayable |
---|---|---|---|
true | false | false | true |
false | true | false | true |
true | true | false | true |
true | true | true | true |
true | false | true | true |
true | true | false | true |
true | true | false | false |
I further propose a naming convention for test methods: each starts with the word test followed by the name of the method being tested, followed by four characters that indicate the top-level configuration we’re employing. So, for example, the first test method for findWindow will be name testFindWindowTFFT. Also, let’s work out way up to a test implementation strategy. That is, instead of showing you all the helper methods and other shortcuts for our final test implementation, let’s look at the process that gets us there.
◼ A Possible Implementation for testFindTFFT
The first thing we have to do is arrange the test data. We want to go looking for every button in the application, and verify that we find all the buttons in dialogs that can be visible or not visible, and displayable. We further have to verify that we fail to find buttons nested in frames, or in disposed dialogs. We’ll start by making two lists, one containing windows with buttons we expect to find, and the other containing windows that we shouldn’t even be looking at:
public void testFindTFFT()
{
List<TestWindow> successWindows =
List.of( visibleDialog, notVisibleDialog );
List<TestWindow> failWindows =
List.of(
disposedDialog,
visibleFrame,
notVisibleFrame,
disposedFrame
);
// ...
}
Next, we can make two more lists, one consisting of the labels on all the push buttons in the first list, and another consisting of all the push buttons in the second list:
// ...
List<String> successLabels =
successWindows.stream()
.flatMap( w -> w.getLabels().stream() )
.collect( Collectors.toList() );
List<String> failLabels =
failWindows.stream()
.flatMap( w -> w.getLabels().stream() )
.collect( Collectors.toList() );
// ...
This should immediately give you an idea for a helper method. Picture yourself making two lists of labels for every remaining test we have for this test class. Surely we can encapsulate that logic in a method that does it for us. Perhaps a varargs method that would be invoked like this: List successLabels = getLabels( visibleDialog, notVisibleDialog );
Suggested exercise: try to implement private static List<String> getLabels( TestWindow… windows ) yourself before looking at the solution, below.
Here’s our helper method, and the first revision of our first test.
private static List<String> getLabels( TestWindow... windows )
{
List<String> list =
Arrays.stream( windows )
.flatMap( w -> w.getLabels().stream() )
.collect( Collectors.toList() );
return list;
}
public void testFindTFFT()
{
List<String> successLabels =
getLabels( visibleDialog, notVisibleDialog );
List<String> failLabels =
getLabels(
disposedDialog,
visibleFrame,
notVisibleFrame,
disposedFrame
);
// ...
}
Next we have to go searching for a push button for every label in the first list and verify that we succeed. Then we have to go looking for every push button in the second list and verify that we fail.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // ... ComponentFinder finder = new ComponentFinder( true, false, false ); for ( String str : successLabels ) { Predicate<JComponent> pred = ComponentFinder.getButtonPredicate( str ); JComponent jComp = finder.find( pred ); assertNotNull( jComp, str ); assertTrue( jComp instanceof JButton, str ); assertEquals( str, ((JButton)jComp).getText() ); } for ( String str : failLabels ) { Predicate<JComponent> pred = ComponentFinder.getButtonPredicate( str ); JComponent jComp = finder.find( pred ); assertNull( jComp, str ); } } |
A couple of quick notes:
- Line 3: For every label in the “success expected” list…
- Lines 6-7: Get a predicate that identifies the target push button.
- Line 8: Look for the target push button.
- Line 9: Verify that find returns a non-null result…
- Line 10: Verify that the result is a JButton…
- Line 11: Verify that the JButton has the expected label.
- Lines 14 – 20: Same as lines 3-10, except:
- Line 19: Verify that find returns a null result.
Now visualize the test method for testFindFTFT (look for components in the window hierarchy where the top-level window: a) is a frame, but not a dialog; b) may or may not be visible; and c) must be displayable). How will it differ from testFindTFFT?
testFindPredicateTFFT
- Has a success list including visible and non-visible dialogs
- Instantiates ComponentFinder with the parameters true, false, false, true
- Processes the success list
- Calls find( Predicate )
- Processes the fail list
- Calls find( Predicate )
testFindPredicateFTFT
- Has a success list including visible and non-visible frames
- Instantiates ComponentFinder with the parameters false, true, false, true
- Processes the success list
- Calls find( Predicate )
- Processes the fail list
- Calls find( Predicate )
Given the preponderance of similarities between the two operations, I’d be disappointed if we couldn’t find a way to combine some of the logic. How about a method that takes a list of dialogs with nested expected-to-find labels, a list dialogs with nested expected-not-to-find labels, and a ComponentFinder and goes from there? My solution, with notes, follows below.
Suggested exercise: try to implement private void testFindPredicateOfJComponent(List<TestWindow> expSuccessWindows, List<TestWindow> expFailWindows, ComponentFinder finder) yourself before looking at the solution, below.
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 | private void testFindPredicateOfJComponent( List<TestWindow> expSuccessWindows, List<TestWindow> expFailWindows, ComponentFinder finder ) { TestWindow[] successArray = expSuccessWindows.toArray( new TestWindow[0] ); TestWindow[] failArray = expFailWindows.toArray( new TestWindow[0] ); List<String> successLabels = getLabels( successArray ); List<String> failLabels = getLabels( failArray ); for ( String str : successLabels ) { Predicate<JComponent> pred = ComponentFinder.getButtonPredicate( str ); JComponent jComp = finder.find( pred ); assertNotNull( jComp, str ); assertTrue( jComp instanceof JButton, str ); assertEquals( str, ((JButton)jComp).getText() ); } for ( String str : failLabels ) { Predicate<JComponent> pred = ComponentFinder.getButtonPredicate( str ); JComponent jComp = finder.find( pred ); assertNull( jComp, str ); } } |
- Lines 7-8: Uses the Arrays class utility to convert a list to an array, which will be used on line 11. But, if you’re not accustomed to it, this line of code looks kind of screwy. That’s because the List interface has two overloads of toArray:
- Object[] toArray()
This method returns an array of Objects, which is less than ideal; we really want an array of type TestWindow. - T[] toArray(T[] a)
This method returns an array of type T. The argument, a, is an array of any length. If it’s big enough, toArray will use it to store the elements of the source list. If it’s not big enough that’s OK; toArray will make an array that’s big enough. So the whole point of allocating a zero-length array in expSuccessWindows.toArray(new TestWindow[0]) is so that toArray will return an array of type TestWindow.
- Object[] toArray()
- Uses the Arrays class utility to convert the expFailWindows list to an array, which will be used on line 12.
- Line 11: Obtains from successArray a list of all the labels contiained in all its constituent TestWindows. (Recall that a Java varargs method, such as someMethod(String… names), is essentially equivalent to a method that takes an array: someMethod(String[] names).)
- Line 12: Obtains from failArray a list of all the labels contiained in all its constituent TestWindows.
- Lines 14-30: Equivalent to lines 4-20 of our original version of this method, above.
Now our next version, not necessarily our final version, of testFindPredicateTFFT looks like this:
@Test
public void testFindPredicateTFFT()
{
List<TestWindow> successWindows =
List.of(
visibleDialog,
notVisibleDialog
);
List<TestWindow> failWindows =
List.of(
disposedDialog,
visibleFrame,
notVisibleFrame,
disposedFrame
);
ComponentFinder finder =
new ComponentFinder( true, false, false );
testFindPredicateOfJComponent(
successWindows,
failWindows,
finder
);
}
Before we finish all of the find(Predicate<JComponent>) test methods, let’s tell a look at one of the findWindow(Predicate<Window>) test methods. Start by looking at the code for testFindPredicateOfJComponent, above. What would be different about a similar helper method for testing findWindow? Some of my thoughts are:
- We’ll be looking for Windows this time, not JButtons, so we won’t need to get lists of labels to look for; instead, we’ll be searching for windows with specific titles.
- We get our predicate by calling ComponentFinder.getWindowPredicate(String title), where “title” is the title of the window we want to find.
- Instead of calling find(Predicate<JComponent>) we’ll be calling findWindow(Predicate<Window>).
And that’s pretty much it. We can make a helper method by copying testFindPredicateOfJComponent and making modest changes. Here is my annotated soultion.
Suggestion: you should try coding private void testFindWindowPredicateOfWindow(List<TestWindow> expSuccessWindows, ComponentFinder finder) yourself before looking at the following solution.
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 void testFindWindowPredicateOfWindow( List<TestWindow> expSuccessWindows, List<TestWindow> expFailWindows, ComponentFinder finder ) { for ( TestWindow testWindow : expSuccessWindows ) { String expTitle = testWindow.getTitle(); Predicate<Window> pred = ComponentFinder.getWindowPredicate( expTitle ); Window window = finder.findWindow( pred ); assertNotNull( window, expTitle ); assertEquals( testWindow.getWindow(), window ); } for ( TestWindow window : expFailWindows ) { String expTitle = window.getTitle(); Predicate<Window> pred = ComponentFinder.getWindowPredicate( expTitle ); Window wind = finder.findWindow( pred ); assertNull( wind, expTitle ); } } |
Notes:
- Line 2: List of TestWindows we expect to pass the top-level parameter configuration in the given ComponentFinder (line 4).
- Line 3: List of TestWindows we expect to fail the top-level parameter configuration in the given ComponentFinder (line 4).
- Line 4: ComponentFinder configured for a specific test. For example testFindWindowTFFT will pass a finder with configured with canBeDialog = true, canBeFrame = false, mustBeVisible = false, mustBeDisplayable = true.
- Line 7: Traverses all the TestWindows in the success expected list.
- Line 9: Gets the title of the window to look for.
- Lines 10-11: Gets a predicate that satisfies the conditions (is-dialog() or is-frame()) and has-title expTitle.
- Line 12: Calls the findWindow method.
- Line 13: Verify that a window was found.
- Line 14: Verifies that the correct window was returned.
- Lines 17-24: Same as above for loop, except:
- Line 17: Traverses the list of windows expected to fail the search.
- Line 23: Verifies no window was found.
And this is what our first version of testFindWindowTFFT might look like.
@Test
public void testFindWindowTFFT()
{
List<TestWindow> successWindows =
List.of(
visibleDialog,
notVisibleDialog
);
List<TestWindow> failWindows =
List.of(
disposedDialog,
visibleFrame,
notVisibleFrame,
disposedFrame
);
ComponentFinder finder =
new ComponentFinder( true, false, false );
testFindWindowPredicateOfWindow(
successWindows,
failWindows,
finder
);
}
◼ Testing Top-level Window Finders: Final Refinements
Let’s take our strategy for testing find(Predicate<JComponent>) and findWindow(Predicate<Window>) one step further. It seems like the main difference between all the find tests is the top-level window configuration. Likewise, the main difference between all the findWindow tests is the top-level window configuration. Can we come up with helper methods (one for find, one for findWindow) that can generate a specific test knowing only the top-level window parameters? Something like:
private void testFindPredicateOfJComponent(
boolean canBeDialog,
boolean canBeFrame,
boolean mustBeVisible,
boolean mustBeDisplayable
)
{
List successWindows = get success-expected windows
List failWindows = get fail-expected windows
ComponentFinder finder = configure component-finder
testFindPredicateOfJComponent( sucess-window, fail-windows, finder )
}
I thnk we can manage something like this. We’ll start with helper methods for compiling the success-expected and fail-expected windows.
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 | private List<TestWindow> getExpSuccessWindows( boolean canBeDialog, boolean canBeFrame, boolean mustBeVisible, boolean mustBeDisplayable ) { List<TestWindow> list = new ArrayList<>(); if ( canBeDialog ) { list.add( visibleDialog ); if ( !mustBeVisible ) list.add( notVisibleDialog ); if ( !mustBeDisplayable ) list.add( disposedDialog ); } if ( canBeFrame ) { list.add( visibleFrame ); if ( !mustBeVisible ) list.add( notVisibleFrame ); if ( !mustBeDisplayable ) list.add( disposedFrame ); } return list; } |
Notes:
- Line 9: If the can-be-dialog parameter is set:
- Line 12: Adds the visibleDialog to the success list.
- Lines 13-14: If it is not necessary for the dialog to be visible, add notVisibleDialog to the success list.
- Lines 13-14: If it is not necessary for the dialog to be visible, add notVisibleDialog to the success list.
- Lines 13-14: If it is not necessary for the dialog to be visible, add notVisibleDialog to the success list.
- Lines 15-16: If it is not necessary for the dialog to be displayable, add disposedDialog to the success list.
- Lines 17-24: Same as lines 9-16, but for frames instead of dialogs.
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 | private List<TestWindow> getExpFailWindows( boolean canBeDialog, boolean canBeFrame, boolean mustBeVisible, boolean mustBeDisplayable ) { List<TestWindow> list = new ArrayList<>(); if ( !canBeDialog ) { list.add( visibleDialog ); list.add( notVisibleDialog ); list.add( disposedDialog ); } else { if ( mustBeVisible ) list.add( notVisibleDialog ); if ( mustBeDisplayable ) list.add( disposedDialog ); } if ( !canBeFrame ) { list.add( visibleFrame ); list.add( notVisibleFrame ); list.add( disposedFrame ); } else { if ( mustBeVisible ) list.add( notVisibleFrame ); if ( mustBeDisplayable ) list.add( disposedDialog ); } return list; } |
Notes:
- Lines 9-14: If the window cannot be a dialog, adds all the dialog test windows to the fail list.
- Lines 15-21: If the window can be a dialog:
- Lines 17-18: If visibility is required, add the notVisible dialog to the fail list.
- Lines 19-20: If displayability is required, add the disposed dialog to the fail list.
- Line 9: If the window cannot be a dialog, add al the dialog test windows to the list.
- Lines 23-35: Same as lines 9-21, but for frames instead of dialogs.
The helper methods with four boolean parameters now look like this (by the way, recall that the mustBeDisplayable top-level parameter cannot be configured via the ComponentFinder constructors).
private void testFindPredicateOfJComponent(
boolean canBeDialog,
boolean canBeFrame,
boolean mustBeVisible,
boolean mustBeDisplayable
)
{
List<TestWindow> successWindows =
getExpSuccessWindows(
canBeDialog,
canBeFrame,
mustBeVisible,
mustBeDisplayable
);
List<TestWindow> failWindows =
getExpFailWindows(
canBeDialog,
canBeFrame,
mustBeVisible,
mustBeDisplayable
);
ComponentFinder finder =
new ComponentFinder( canBeDialog, canBeFrame, mustBeVisible );
finder.setMustBeDisplayable( mustBeDisplayable );
testFindPredicateOfJComponent(
successWindows,
failWindows,
finder
);
}
private void testFindWindowPredicateOfWindow(
boolean canBeDialog,
boolean canBeFrame,
boolean mustBeVisible,
boolean mustBeDisplayable
)
{
List<TestWindow> successWindows =
getExpSuccessWindows(
canBeDialog,
canBeFrame,
mustBeVisible,
mustBeDisplayable
);
List<TestWindow> failWindows =
getExpFailWindows(
canBeDialog,
canBeFrame,
mustBeVisible,
mustBeDisplayable
);
ComponentFinder finder =
new ComponentFinder( canBeDialog, canBeFrame, mustBeVisible );
finder.setMustBeDisplayable( mustBeDisplayable );
testFindWindowPredicateOfWindow(
successWindows,
failWindows,
finder
);
}
Here are a couple of out finel find(Predicate<JComponent>) and findWindow(Predicate<Window>) test methods (see also Testing Top Level Window Methods, above). The complete code can be found in the GitHub repository.
@Test
public void testFindPredicateTTTT()
{
testFindPredicateOfJComponent( true, true, true, true );
}
@Test
public void testFindPredicateTFTT()
{
testFindPredicateOfJComponent( true, false, true, true );
}
@Test
public void testFindWindowTFFT()
{
testFindWindowPredicateOfWindow( true, true, true, true );
}
◼ Testing find(Window, Predicate<JComponent>) and find(JComponent, Predicate<JComponent>)
The remaining two methods we have to test are much more straightforward than the last few tests, because we don’t have to worry about top-level window configurations. All we have to do is:
- Choose any two test windows; label them success-window and fail-window.
- Verify that you can find all the push buttons in the success window, passing:
- successWindow.getWindow() for find(Window,Predicate), or
- successWindow.getContentPane() for find(JComponent,Predicate)
- Verify that you can none of the push buttons in the fail window, passing:
- successWindow.getWindow() for find(Window,Predicate), or
- successWindow.getContentPane() for find(JComponent,Predicate)
We’ll have two test methods, one for find(Window,Predicate) and one for find(JComponent, Predicate). See below.
@Test
public void testFindPredicateTTTT()
{
testFindPredicateOfJComponent( true, true, true, true );
}
@Test
public void testFindPredicateTFTT()
{
testFindPredicateOfJComponent( true, false, true, true );
}
@Test
public void testFindWindowTFFT()
{
testFindWindowPredicateOfWindow( true, true, true, true );
}
Summary
In this lesson we are developing a facility for interrogating an application’s GUI. On the previous page we designed an wrote the implementation of this facility. On this page we developed a comprehensive, automated unit test for it. On the next page we will use the facility to revise previously written unit tests for other application components that display dialogs.