In this lesson we will write JUnit tests for the ColorSelector and ColorEditor facilities we developed on the previous page.
GitHub repository: Cartesian Plane Part 15
Previous lesson: Cartesian Plane Lesson 15 Page 5: The ColorEditor Class
The ColorSelector Dialog Unit Test
If we display the ColorSelector dialog (see ColorSelectorDemo1 in the project sandbox) it looks like there’s an awful lot going on. And I suppose there is, but most of the work is being handled by the JColorChooser component. We need only concentrate on testing the following:
- Constructors;
- The showDialog method:
- Does the dialog appear on the screen when we call this method?
- Does the method return the selected color when dismissed via the OK button?
- Does it return null when dismissed any other way?
■ Infrastructure: Fields and Helper Methods
Like similar test drivers we’ve seen, we’re going to need helper methods to start the color selector dialog in a new thread, interrogate the dialog to find the OK and Cancel buttons, etc. The value returned (in a separate thread) by showDialog will have to be stored in an instance variable, as will the components extracted from the dialog. Our instance variables for this test look like this:
private ColorSelector colorSelector;
private JDialog chooserDialog;
private JColorChooser chooser;
private JButton chooserOKButton;
private JButton chooserCancelButton;
private Color selectedColor;
The colorSelector field is created in a beforeEach method for the convenience of subsequent tests. The dialog is started and made visible in a separate thread in the showDialog helper method, after which the chooserDialog, chooser, chooserOKButton and chooserCancelButton are identified. Here are the beforeEach, showDialog and showColorSelector helper methods, followed by 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 | @BeforeEach public void beforeEach() { GUIUtils.schedEDTAndWait( () -> colorSelector = new ColorSelector() ); } private Thread showColorSelector() { assertNotNull( colorSelector ); Thread thread = new Thread( () -> showDialog() ); thread.start(); Utils.pause( 250 ); GUIUtils.schedEDTAndWait( () -> { getChooserDialog(); getChooser(); getChooserOKButton(); getChooserCancelButton(); }); return thread; } private void showDialog() { GUIUtils.schedEDTAndWait( () -> selectedColor = colorSelector.showDialog() ); } |
- Lines 4-6: Schedules construction of the ColorSelector on the EDT and waits for it to complete.
- Line 11: Sanity check; we are assuming that the ColorSelector has been instantiated prior to this method being called.
- Line 12: Creates the the thread that will start the ColorSelector dialog using colorSelector.showDialog().
- Line 13: Schedules execution of the thread created on line 12.
- Line 14: Pauses to give the new thread a chance to start, and the ColorSelector dialog a chance to become visible.
- Lines 15-20: Retrieves from the ColorSelector dialog those components which are needed for testing. Because retrieval of a component requires multiple steps each component is retrieved using a separate helper method. Two of these methods are shown, below.
- Line 21: Returns the thread created on line 12 to the caller. Presumably the caller will set up a test, then push one of the dialog’s buttons, causing the dialog to become invisible, and the colorSelector.showDialog method to return.
- Lines 24-29: The showDialog method which is called from a separate thread initiated by the showColorSelector method.
- Lines 26-28: Schedules colorSelector.showDialog() to be executed on the EDT, and waits for it to complete. The value returned by colorSelector.showDialog() is stored in an instance variable.
Note: The showDialog() method consists of a single line of code, and is present for aesthetic purposes. Because it’s a single line of code many programmers are going to want to integrate it into the calling method, showColorSelector(). This can be done, but it’s kind of ugly:
Thread thread = new Thread(
() -> GUIUtils.schedEDTAndWait(
() -> selectedColor = colorSelector.showDialog()
)
);
In my opinion the improved readability is worth the extra bit of code.
Here are two of the helper methods utilized by showColorSelector(); the complete code can be found in the GitHub repository.
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 void getChooserDialog() { boolean canBeDialog = true; boolean canBeFrame = false; boolean mustBeVisible = true; ComponentFinder finder = new ComponentFinder( canBeDialog, canBeFrame, mustBeVisible ); Window comp = finder.findWindow( c -> true ); assertNotNull( comp ); assertTrue( comp instanceof JDialog ); chooserDialog = (JDialog)comp; } private void getChooser() { // Assume getChooserDialog called first assertNotNull( chooserDialog ); JComponent comp = ComponentFinder.find( chooserDialog, c -> (c instanceof JColorChooser) ); assertNotNull( comp ); assertTrue( comp instanceof JColorChooser ); chooser = (JColorChooser)comp; } |
■ Testing the Constructors:
- testColorSelector()
- testColorSelectorColor( int iColor )
- public void testColorSelectorWindowStringColor( int iColor )
Testing the constructors is mainly an exercise in instantiating a ColorSelector, calling ColorSelector.showDialog(), and verifying that a ColorSelector dialog shows up on the screen. The test for the default constructor looks like this:
@Test
public void testColorSelector()
{
Thread thread = showColorSelector();
GUIUtils.schedEDTAndWait( () -> {
assertTrue( chooserDialog.isVisible() );
chooserOKButton.doClick();
});
Utils.join( thread );
}
There are two constructors with an initial color parameter. These tests for these constructors want to make sure that, when the color selector dialog is displayed, the given color is selected. There’s a slight chance that the given initial color will match the selector’s default color. To make sure we avoid this issue, we’ll run the test several times using different colors. Here’s the test for the one-parameter constructor:
@ParameterizedTest
@ValueSource(ints = {1, 3, 5})
public void testColorSelectorColor( int iColor )
{
Color color = new Color( iColor );
GUIUtils.schedEDTAndWait( () ->
colorSelector = new ColorSelector( color )
);
Thread thread = showColorSelector();
GUIUtils.schedEDTAndWait( () -> {
assertTrue( chooserDialog.isVisible() );
chooserOKButton.doClick();
});
Utils.join( thread );
assertEquals( color, selectedColor );
}
There’s one more constructor that has two parameters, an initial color and a dialog title. As an exercise, you should try to write this test yourself. The solution can be found in the GitHub repository.
■ Testing Dialog Behavior on Dismissal
- testShowDialogOK()
- testShowDialogCancel()
The last two tests verify that the ColorSelector dialog works correctly when dismissed with either the OK or Cancel button. To test the OK button, we’ll initialize the ColorSelector with a known color, start the dialog, select a unique color, and verify that the selected, unique color is returned by the showDialog() method. Testing the Cancel button is even easier; no color selection is necessary, we only have to verify that showDialog returns null. Here’s the code for testing the OK button; you should try writing a test for the Cancel button yourself. All the code can be found in the GitHub repository.
@Test
public void testShowDialogOK()
{
Color initColor = Color.RED;
Color uniqueColor = Color.BLUE;
GUIUtils.schedEDTAndWait( () ->
colorSelector = new ColorSelector( initColor )
);
Thread thread = showColorSelector();
GUIUtils.schedEDTAndWait( () -> {
assertTrue( chooserDialog.isVisible() );
chooser.setColor( uniqueColor );
chooserOKButton.doClick();
});
Utils.join( thread );
assertEquals( uniqueColor, selectedColor );
}
The ColorEditor Unit Test
The ColorEditor class has a lot of moving parts. A summary of the major features to be tested is:
- Constructor: Mainly we care if the constructor crashes or not. We’ll go a little farther and make sure the three constituent components (color button, color editor and feedback window) are present after construction.
- GUI Components:
- Do the three component getters return reasonable values?
- If we push the color button and select a new color, is the new color reflected in the text editor and the feedback window?
- If we type a valid color value (an integer) into the text editor, is the feedback window correctly updated?
- If we type an invalid value into the text editor, is the invalid value correctly handled?
- Property Accessors:
- Does getColor() return the correct value when a color is correctly selected in the ColorEditor?
- Does getColor() return the correct value when an invalid value is entered in the text editor?
- Does setColor() correctly update the selected color, including configuring the text and feedback components?
- Dialog Interaction:
- Does the color selector dialog pop up when the color button is pushed?
- Does the dialog return the selected color when dismissed with the OK button?
- Does the dialog return null when dismissed with the Cancel button?
- Action Listeners:
- Are all ActionListeners notified when we select a new color in the color selector dialog? When we enter a new color in the text editor? When we call ColorEditor.setColor?
- If I remove an ActionListener is it correctly not notified when an ActionEvent is propagated?
About Running the Dialog Thread: This operation will execute just like similar ones we’ve seen in the past: the unit test method gets the thread object, configures and dimisses the dialog, and waits for the thread to terminate (it joins the dialog thread). Also a great deal of the test will have to be executed on the EDT; but we have to be careful not to join the dialog thread while we’re running on the EDT. That’s because the EDT is processing the dialog dismissal; and joining the dialog thread from the EDT would suspend the EDT while waiting for the EDT to finish its work… which it can’t do while it’s suspended. Multithreaded programming can be tricky.
■ Infrastructure: Fields and Helper Methods
Here we have yet another test involving dialogs. We’re going to need logic that starts a dialog in a separate thread by clicking the color button. We’ll need instance variables to store GUI components, such as the three components embedded in the ColorEditor instance, and the selector dialog’s OK and Cancel buttons. To test the ActionListeners we will need instance variables for the listeners, which execute on the EDT, to communicate with the test code, running on the test thread. Here’s an accounting of our instance variables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private ColorEditor defEditor; private JTextField textEditor; private JButton colorButton; private JComponent feedbackWindow; private JDialog chooserDialog; private JColorChooser chooser; private JButton chooserOKButton; private JButton chooserCancelButton; private boolean actionListener1Fired; private boolean actionListener2Fired; private Color feedbackColor;; |
- Line 1: The default ColorEditor. Created by the before-each method for the convenience of the unit tests.
- Lines 3-5: The three components embedded in defEditor; initialized by the before-each method after creating defEditor.
- Line 7: The selector dialog displayed when the color button is pushed; initialized in startColorChooser().
- Line 8: The JColorChooser component of the selector dialog; initialized in startColorChooser().
- Lines 9,10: The OK and Cancel button components of the selector dialog; initialized in startColorChooser().
- Lines 12,13: Variables used to indicate whether or not an ActionListener was invoked; used in testing the add/removeActionListener methods.
Here is the before-each method that does most of the test initialization. I’ve also included the after-each method, which performs clean up of miscellaneous top-level windows created during testing.
@BeforeEach
public void beforeEach()
{
GUIUtils.schedEDTAndWait( () -> {
defEditor = new ColorEditor();
textEditor = defEditor.getTextEditor();
colorButton = defEditor.getColorButton();
feedbackWindow = defEditor.getFeedback();
});
actionListener1Fired = false;
actionListener2Fired = false;
feedbackColor = null;
}
@AfterEach
public void afterEach()
{
ComponentFinder.disposeAll();
}
*About feedbackColor: Recall that you cannot change the value of a local variable from an anonymous class; recall also that a lambda is an anonymous class. That makes code like this illegal:
// This code won't compile!!!
private Color getFeedbackColor()
{
Color color = null;
GUIUtils.schedEDTAndWait( () ->
color = feedbackWindow.getBackground()
);
return color;
}
However there’s no rule against setting instance variables from a lambda, so we can substitute this code:
// This code will compile
private Color getFeedbackColor()
{
GUIUtils.schedEDTAndWait( () ->
feedbackColor= feedbackWindow.getBackground()
);
return feedbackColor;
}
Let’s look next at startColorChooser(), the method that pushes the color button, thereby posting the color selector dialog. As we’ve seen before, the dialog is started in its own thread. After starting the thread, startColorChooser() pauses to give the dialog enough time to be displayed, then it digs out the dialog components we need for testing (the OK and Cancel buttons, for instance). Finally, it returns the Thread object to the caller. Note that there’s a separate helper method for fetching each component; this makes the code easier to read.
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 | private void showDialog() { GUIUtils.schedEDTAndWait( () -> colorButton.doClick() ); } private Thread startColorChooser() { Thread thread = new Thread( () -> showDialog(), "ColorChooserThread" ); thread.start(); Utils.pause( 250 ); GUIUtils.schedEDTAndWait( () -> { getChooserDialog(); getChooser(); getChooserOKButton(); getChooserCancelButton(); }); return thread; } private void getChooserDialog() { boolean canBeDialog = true; boolean canBeFrame = false; boolean mustBeVisible = true; ComponentFinder finder = new ComponentFinder( canBeDialog, canBeFrame, mustBeVisible ); Window comp = finder.findWindow( w -> (w instanceof JDialog) ); assertNotNull( comp ); assertTrue( comp instanceof JDialog ); chooserDialog = (JDialog)comp; } private void getChooser() { // Assume getColorDialog called first JComponent comp = ComponentFinder.find( chooserDialog, c -> (c instanceof JColorChooser) ); assertNotNull( comp ); assertTrue( comp instanceof JColorChooser ); chooser = (JColorChooser)comp; } |
We have several convenience methods for dealing with colors in general, and the color of the feedback window in particular. The first one is getRGB(). You’ll remember from previous lessons that, in addition to the red, green and blue values, Color.getRGB() returns the alpha value, and that it’s convenient for us to suppress it. That’s all this method does; its input is an ARGB value, and its output is an RGB value (with the alpha bits turned off).
private int getRGB( Color color )
{
int rgb = color.getRGB() & 0xffffff;
return rgb;
}
The remaining color methods deal specifically with the color of the feedback window. Method getFeedbackColor() gets the color of the feedback window. This method can be called from any thread; if necessary, it will schedule itself to run on the EDT. Method getFeedbackRGB() gets the RGB value of the feedback window color (with the alpha bits turned off), and getUniqueRGB() returns a color that is guaranteed to be different from the color currently in the feedback window. Note that getFeedbackRGB() and getUniqueRGB() are safe to be called from any thread.
Digression: The exclusive or (^) of two boolean values is true if exactly one of the operands is true:
TRUE ^ FALSE = TRUE FALSE ^ TRUE = TRUE TRUE ^ TRUE = FALSE FALSE ^ FALSE = FALSE
In bitwise logic, 0 is taken to be FALSE and 1 is taken to be TRUE, so the bitwise exclusive-or of blue and medium gray will be:
0000 0000 1111 ^ 0101 0101 0101 ---------------- 0101 0101 1010
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | private Color getFeedbackColor() { if ( SwingUtilities.isEventDispatchThread() ) feedbackColor = feedbackWindow.getBackground(); else GUIUtils.schedEDTAndWait( () -> feedbackColor = feedbackWindow.getBackground() ); return (Color)feedbackColor; } private int getFeedbackRGB() { int rgb = getRGB( getFeedbackColor() ); return rgb; } private int getUniqueRGB() { int fRGB = getFeedbackRGB(); int rgb = fRGB ^ 0xff; assertNotEquals( fRGB, rgb ); return rgb; } |
We have one more helper method: commitEdit() simulates the operator pressing the enter key in the colorEditor, which results in processing its content; note that this method can be called from any thread.
private void commitEdit()
{
if ( SwingUtilities.isEventDispatchThread() )
textEditor.postActionEvent();
else
GUIUtils.schedEDTAndWait( () -> textEditor.postActionEvent() );
}
■ Test Method testColorEditor
This method tests the ColorEditor constructor. It’s pretty simple; mostly it verifies that instantiating ColorEditor doesn’t crash the application.
@Test
public void testColorEditor()
{
assertNotNull( textEditor );
assertNotNull( colorButton );
assertNotNull( feedbackWindow );
}
■ Testing the addActionListener Method:
- testAddActionListenerColorEditor
This method verifies that ActionListeners added to a ColorEditor object fire when a change is committed to the color editor. - testAddActionListenerColorSelector
This method verifies that ActionListeners added to a ColorEditor object fire when a color is selected from the ColorSelector. - testAddActionListenerSetColor
This method verifies that ActionListeners added to a ColorEditor object fire when ColorEditor.setColor is invoked.
Here are the tests for test the ActionListener facility when a change is committed to the color editor, and a new color is selected in the ColorSelector. You should try to write the test for ColorEditor.setColor before looking at the solution in the GitHub repository.
public void testAddActionListenerColorEditor()
{
ActionListener listener1 = e -> actionListener1Fired = true;
ActionListener listener2 = e -> actionListener2Fired = true;
GUIUtils.schedEDTAndWait( () -> {
defEditor.addActionListener( listener1 );
commitEdit();
assertTrue( actionListener1Fired );
assertFalse( actionListener2Fired );
actionListener1Fired = false;
actionListener2Fired = false;
defEditor.addActionListener( listener2 );
commitEdit();
assertTrue( actionListener1Fired );
assertTrue( actionListener2Fired );
});
}
@Test
public void testAddActionListenerColorSelector()
{
Thread thread = startColorChooser();
actionListener1Fired = false;
GUIUtils.schedEDTAndWait( () -> {
defEditor.addActionListener( e -> actionListener1Fired = true );
chooserOKButton.doClick();
commitEdit();
});
Utils.join( thread );
assertTrue( actionListener1Fired );
}
■ Test Method testRemoveActionListener
This method verifies that, when requested, ActionListeners are correctly removed from a ColorEditor instance. Add a listener and make sure it fires when a color is changed, then remove the listener and verify that it does not fire when a color is changed.
@Test
public void testRemoveActionListener()
{
ActionListener listener1 = e -> actionListener1Fired = true;
ActionListener listener2 = e -> actionListener2Fired = true;
GUIUtils.schedEDTAndWait( () -> {
defEditor.addActionListener( listener1 );
defEditor.addActionListener( listener2 );
commitEdit();
assertTrue( actionListener1Fired );
assertTrue( actionListener2Fired );
actionListener1Fired = false;
actionListener2Fired = false;
defEditor.removeActionListener( listener2 );
commitEdit();
assertTrue( actionListener1Fired );
assertFalse( actionListener2Fired );
actionListener1Fired = false;
actionListener2Fired = false;
defEditor.removeActionListener( listener1 );
commitEdit();
assertFalse( actionListener1Fired );
assertFalse( actionListener2Fired );
});
}
■ Test Method testGetPanel
All we’re going to do here is verify that getPanel returns a JPanel containing the color button, text editor and feedback window.
@Test
public void testGetPanel()
{
// Just make sure we get back a JPanel containing all three
// ColorEditor components somewhere in the panel's window hierarchy.
Predicate<JComponent> isTextEditor = c -> (c == textEditor);
Predicate<JComponent> isColorButton = c -> (c == colorButton);
Predicate<JComponent> isFeedback = c -> (c == feedbackWindow);
GUIUtils.schedEDTAndWait( () -> {
JPanel panel = defEditor.getPanel();
assertNotNull( ComponentFinder.find( panel, isTextEditor ) );
assertNotNull( ComponentFinder.find( panel, isColorButton ) );
assertNotNull( ComponentFinder.find( panel, isFeedback ) );
});
}
■ Test Methods testGetTextEditor, testGetFeedback
These methods are going to get the text editor and feedback windows, and verify that they are synchronized.
@Test
public void testGetTextEditor()
{
// Change the value of the text editor, then verify that
// the change is reflected in the feedback window.
GUIUtils.schedEDTAndWait( () -> {
int testRGB = getUniqueRGB();
textEditor.setText( "" + testRGB );
commitEdit();
assertEquals( testRGB, getFeedbackRGB() );
});
}
@Test
public void testGetFeedback()
{
// Make sure that the color of the feedback window is the
// same as the one in the text editor.
GUIUtils.schedEDTAndWait( () -> {
int feedbackRGB = getFeedbackRGB();
String strRGB = textEditor.getText();
int textRGB = Integer.decode( strRGB );
assertEquals( textRGB, feedbackRGB );
});
}
■ Test Method testGetColorButton
Get the color button from the ColorEditor, and verify that pushing it brings up the color selector. This runs as a ParameterizedTest. With each pass it picks a new color in the color selector, pushes the OK button, and verifies that the chosen color is reflected in the feedback window.
@ParameterizedTest
@ValueSource(ints = {1, 3, 5})
public void testGetColorButton( int iRGB )
{
// startColorSelector pokes the colorButton which causes
// the chooser dialog to be posted.
Thread thread = startColorChooser();
Color testColor = new Color( iRGB );
GUIUtils.schedEDTAndWait( () -> {
chooser.setColor( testColor );
chooserOKButton.doClick();
});
Utils.join( thread );
GUIUtils.schedEDTAndWait( () ->
assertEquals( iRGB, getFeedbackRGB() )
);
}
■ Test Method testSelectColorAndCancel
This method will run several times. It initializes the ColorEditor with a known color. Then it starts the ColorSelector (by pushing the color button), selects a color guaranteed to be different from the initial color, and pushes the selector’s Cancel button. It then verifies that the color stored in the feedback window has not changed from its initial value.
@ParameterizedTest
@ValueSource(ints = {1, 3, 5})
public void testSelectColorAndCancel( int iRGB )
{
// Set color of ColorEditor to known value. Start
// color chooser and choose given color; poke cancel button
// and verify that the color in ColorEditor is not changed.
Thread thread = startColorChooser();
Color origColor = new Color( iRGB );
Color testColor = Color.RED;
GUIUtils.schedEDTAndWait( () -> {
defEditor.setColor( origColor );
chooser.setColor( testColor );
chooserCancelButton.doClick();
});
Utils.join( thread );
GUIUtils.schedEDTAndWait( () -> {
assertEquals( iRGB, getFeedbackRGB() );
});
}
■ Test Method testGetColorFromText
This test will change the color value in the text editor and commit the change. Then it verifies that the color in the feedback window has changed to the same value as that stored in the text editor.
@Test
public void testGetColorFromText()
{
GUIUtils.schedEDTAndWait( () -> {
int testRGB = getUniqueRGB();
textEditor.setText( "" + testRGB );
commitEdit();
int actRGB = getFeedbackRGB();
assertEquals( testRGB, actRGB );
});
}
■ Test Method testGetColorFromSelector
This method selects a new color in the color selector and pushes the selector’s OK button. Then it verifies that the color stored in the ColorEditor and the color displayed in the feedback window matches the selected color.
@Test
public void testGetColorFromSelector()
{
int testRGB = getUniqueRGB();
Color testColor = new Color( testRGB );
Thread thread = startColorChooser();
GUIUtils.schedEDTAndWait( () -> {
chooser.setColor( testColor );
chooserOKButton.doClick();
});
Utils.join( thread );
GUIUtils.schedEDTAndWait( () -> {
Color actColor = defEditor.getColor().orElse( null );
assertNotNull( actColor );
assertEquals( testColor, actColor );
Color fbColor = getFeedbackColor();
assertEquals( testColor, fbColor );
});
}
■ Test Method testEditTextNeg
This is a negative test for the text editor. We’ll enter an invalid value in the text editor, commit it, and verify that a) the text editor displays an error message; and b) the color of the feedback window doesn’t change.
@Test
public void testEditTextNeg()
{
// Enter an invalid value into the text editor and commit.
// verify that the text editor displays "ERROR", and that
// the color of the feedback window doesn't change.
GUIUtils.schedEDTAndWait( () -> {
Color origColor = getFeedbackColor();
textEditor.setText( "invalid" );
commitEdit();
String actText = textEditor.getText().toUpperCase();
assertTrue( actText.contains( "ERROR" ) );
assertEquals( origColor, getFeedbackColor() );
});
}