Cartesian Plane Lesson 18 Page 17: ProfileEditor JUnit Tests

ProfileEditor, ProfileEditorTestGUI, JUnit

On this page, we’ll examine ProfileEditorTest, the JUnit test class for the ProfileEditor class. It includes ProfileEditorTestGUI, a support class for managing the test GUI and ensuring that GUI-related operations are executed on the EDT. We’ll begin our discussion with the test GUI.

GitHub repository: Cartesian Plane Lesson 18

Previous page: Cartesian Plane Lesson 18 Page 16: ProfileEditorTestBase

Class ProfileEditorTestGUI

This class supports the ProfileEditor JUnit test. It is a concrete subclass of ProfileEditorTestBase, implemented as a singleton and located in the …test_utils package on the test source path. It has one field, a class reference to its singleton. It has a public class method, getTestGUI, which returns the singleton, instantiating it if necessary. It can optionally be invoked in the context of the EDT. If not, the method ensures that the singleton is instantiated on the EDT. The private constructor creates the ProfileEditor to test and places it in a test frame. The frame’s content pane is a JPanel with a BorderLayout and the ProfileEditor under test in its center position. Following is a listing of the ProfileEditorTestGUI class in its entirety.

public class ProfileEditorTestGUI extends ProfileEditorTestBase
{    
    private static ProfileEditorTestGUI testGUI;
    public static ProfileEditorTestGUI getTestGUI( Profile profile )
    {
        if ( testGUI != null )
            ;
        else if ( SwingUtilities.isEventDispatchThread() )
            testGUI = new ProfileEditorTestGUI( profile );
        else
            GUIUtils.schedEDTAndWait( () -> 
                testGUI = new ProfileEditorTestGUI( profile )
            );
        return testGUI;
    }
    private ProfileEditorTestGUI( Profile profile )
    {
        super( new ProfileEditor( profile ) );

        String      title           = "ProfileEditor Test GUI";
        JFrame      editorFrame     = new JFrame( title );
        JPanel      contentPane     = new JPanel( new BorderLayout() );
        ProfileEditor   profileEditor   = getProfileEditor();
        contentPane.add( profileEditor, BorderLayout.CENTER );
        
        editorFrame.setContentPane( contentPane );
        editorFrame.pack();
        editorFrame.setVisible( true );
    }
}

Class ProfileEditorTest

Looking at the ProfileEditor GUI, your first thought might be: “This will be ugly.” It would be ugly if we had to validate the display after every property update, but we’ve already done that with the ProfileEditorFeedback unit test. Here, we are mainly interested in testing the GUI’s interaction with the embedded Profile.

The ProfileEditor class has one public constructor and three public methods: getFeedback, apply and reset. The getFeedback method is a simple getter. To test apply, we have to set the values of all the components to unique values, execute the ProfileEditor’s apply method, and ensure all the values are copied from the components to the Profile. To test the reset method, we set all the components to unique values, execute ProfileEditor.reset(), and verify that all property values are copied from the profile to the components.

Before proceeding to its three principal test methods, we’ll discuss the test class’s infrastructure.

ProfileEditorTest Class Infrastructure

The infrastructure for this class consists of its fields, before and after methods, and helper methods.

⬛ Fields
Following is an annotated list of the ProfileEditorTest class’s fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ProfileEditorTest
{
    private static final String axesSetName         =
        LinePropertySetAxes.class.getSimpleName();
    private static final String gridLinesSetName    =
        LinePropertySetGridLines.class.getSimpleName();
    private static final String ticMajorSetName     =
        LinePropertySetTicMajor.class.getSimpleName();
    private static final String ticMinorSetName     =
        LinePropertySetTicMinor.class.getSimpleName();
    private static final String[]   allSetNames     =
    { axesSetName, gridLinesSetName, ticMajorSetName, ticMinorSetName };

    private static final Profile    baseProfile     = new Profile();
    private static final Profile    distinctProfile = 
        ProfileUtils.getDistinctProfile( baseProfile );
    private static final ProfileEditorTestGUI   testGUI =
        ProfileEditorTestGUI.getTestGUI( new Profile() );
    // ...
}
  • Lines 3-10: These are the simple names of the LinePropertySet subclasses, declared here for convenience.
  • Lines 11,12: An array containing the simple names of the LinePropertySet subclasses, declared here for convenience. See applyDistinctProperties and validateCurrState.
  • Line 14: The baseProfile consists of Profile property values copied from the PropertyManager at the program’s start and never changed. It is used before each test to return the ProfileManager to its original state; see beforeEach.
  • Lines 15,16: The distinctProfile consists of property values guaranteed to be different from those in the baseProfile and, as much as possible, from each other. It is initialized at the start of the program and never changed.
  • Lines 17,18: The test GUI.

⬛ Before- and After- Methods
Following is a discussion of the ProfileEditorTest class’s before– and after-methods.

🟦 @BeforeEach public void beforeEach() throws Exception
At the start of each test method execution, this method (see below) restores the PropertyManager and GUI components to their initial values. Line 6 is a sanity check, verifying that the states of the GUI and the baseProfile agree.

1
2
3
4
5
6
7
@BeforeEach
public void beforeEach() throws Exception
{
    baseProfile.apply();
    testGUI.reset();
    validateCurrState( baseProfile );
}

🟦 @AfterEach public void afterEach()
After each test method completes execution, the afterEach method ensures that the FontEditorDialog is closed. This will be necessary if a test failure terminates a test method, leaving the dialog open. Here’s the code.

@AfterEach
public void afterEach()
{
    testGUI.cancelFontDialog();
}

🟦 public void afterAll()
After all test methods complete execution, this method restores the PropertyManager to its original state and disposes all dialogs. This is necessary if this JUnit test is executed as part of a suite because state initialization does not automatically occur between tests. The code looks like this.

@AfterAll
public static void afterAll()
{
    baseProfile.apply();
    ComponentFinder.disposeAll();
}

⬛ Private Methods
Following is a discussion of the ProfileEditorTest class’s private methods.

🟦 private int getRGB( Color color )
This method obtains the integer value of the given color with the alpha bits stripped. The implementation is an exercise for the student; the solution is in the GitHub repository.

🟦 private void applyDistinctGraphProperties()
This method changes the values of all GUI components that manage GraphPropertySetMW properties to those contained in the distinctProfile. Here’s the annotated code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private void applyDistinctGraphProperties()
{
    GraphPropertySet    graphSet    = distinctProfile.getMainWindow();
    int                 iFGColor    = getRGB( graphSet.getFGColor() );
    int                 iBGColor    = getRGB( graphSet.getBGColor() );
    
    testGUI.setFontDraw( graphSet.isFontDraw() );
    testGUI.setBGColor( iBGColor );
    testGUI.setGridWidth( graphSet.getWidth() );
    
    Thread  thread  = testGUI.editFont();
    testGUI.setFGColor( iFGColor );
    testGUI.setFontBold( graphSet.isBold() );
    testGUI.setFontItalic( graphSet.isItalic() );
    testGUI.setFontName( graphSet.getFontName() );
    testGUI.setFontSize( graphSet.getFontSize() );
    testGUI.selectFDOK();
    Utils.join( thread );
}
  • Line 3: From the Profile of distinct property values, get the GraphPropertySet.
  • Lines 4,5: Get the font and grid colors’ integer values.
  • Lines 7-9: Set the values of the components in the Grid panel.
  • Line 11: Start the FontEditorDialog.
  • Lines 12-16: Set the values of the components associated with font properties.
  • Line 17: Select the FontEditorDialog’s OK button.
  • Line 18: Wait for the FontEditorDialog to close.

🟦 private void applyDistinctLineProperties( String propSet )
This method changes the values of all GUI components that manage the given LinePropertySet (propSet) to those contained in the distinctProfile. Only properties that are supported by the given LinePropertySet are considered. Here’s the code.

private void applyDistinctLineProperties( String propSet )
{
    LinePropertySet lineSet = 
        distinctProfile.getLinePropertySet( propSet );
    if ( lineSet.hasColor() )
    {
        Color   color   = lineSet.getColor();
        int     rgb     = getRGB( color );
        testGUI.setColor( propSet, rgb );
    }
    if ( lineSet.hasDraw() )
        testGUI.setDraw( propSet, lineSet.getDraw() );
    if ( lineSet.hasLength() )
        testGUI.setLength( propSet, lineSet.getLength() );
    if ( lineSet.hasSpacing() )
        testGUI.setSpacing( propSet, lineSet.getSpacing() );
    if ( lineSet.hasStroke() )
        testGUI.setStroke( propSet, lineSet.getStroke() );
}

🟦 private void applyDistinctProperties()
Set the values of all GUI components that edit Profile properties to distinct values. First, we set the components for properties contained directly in the Profile (name, gridUnit), then for the properties in the GraphPropertySet, and finally for the components that map to the four LinePropertySet subclasses. Here’s the code.

private void applyDistinctProperties()
{
    testGUI.setName( distinctProfile.getName() );
    testGUI.setGridUnit( distinctProfile.getGridUnit() );
    applyDistinctGraphProperties();
    Stream.of( allSetNames )
        .forEach( this::applyDistinctLineProperties );
}

🟦 private void collectGraphProperties( Profile profile )
Interrogate all GUI components that edit GraphPropertySet properties, copying their values to the given Profile. The annotated listing for this method follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void collectGraphProperties( Profile profile )
{
    // Make sure the font dialog is initialized
    Thread  thread  = testGUI.editFont();
    testGUI.selectFDCancel();
    Utils.join( thread );
    
    GraphPropertySet    graphSet    = profile.getMainWindow();
    Color               bgColor     =
        new Color( testGUI.getBGColor() );
    Color               fgColor     =
        new Color( testGUI.getFGColor() );
    
    graphSet.setWidth( testGUI.getGridWidth() );
    graphSet.setBGColor( bgColor );
    graphSet.setBold( testGUI.getFontBold() );
    graphSet.setFGColor( fgColor );
    graphSet.setFontDraw( testGUI.getFontDraw() );
    graphSet.setFontName( testGUI.getFontName() );
    graphSet.setFontSize( testGUI.getFontSize() );
    graphSet.setItalic( testGUI.getFontItalic() );
}
  • Lines 4-6: Before we interrogate the FontEditorDialog, make sure all its components are initialized; they don’t have to be visible, just initialized.
    • Line 4: Start the dialog and wait for it to become visible. The dialog’s components will automatically be initialized.
    • Line 5: Dismiss the dialog.
    • Line 6: Wait for the dialog to close.
  • Line 8: Get the GraphPropertySet from the given profile.
  • Lines 9-21: Get the values of the components that edit properties in the GraphPropertySet and transfer them to graphSet.

🟦 private void collectLineProperties( String propSet, Profile profile )
For the LinePropertySet with the given name, interrogate all GUI components that edit the encapsulated properties, copying their values to the given Profile. Only components that edit supported properties are considered. The listing for this method follows.

private void collectLineProperties( String propSet, Profile profile )
{
    LinePropertySet lineProperties  = 
        profile.getLinePropertySet( propSet );
    if ( lineProperties.hasColor() )
    {
        Color   color   = new Color( testGUI.getColor( propSet ) );
        lineProperties.setColor( color );
    }
    if ( lineProperties.hasDraw() )
        lineProperties.setDraw( testGUI.getDraw( propSet ) );
    if ( lineProperties.hasLength() )
        lineProperties.setLength( testGUI.getLength( propSet ) );
    if ( lineProperties.hasSpacing() )
        lineProperties.setSpacing( testGUI.getSpacing( propSet ) );
    if ( lineProperties.hasStroke() )
        lineProperties.setStroke( testGUI.getStroke( propSet ) );
}

🟦 private void validateCurrState( Profile expState )
Verify that the values of the GUI components for editing Profile properties match the expected values provided in the given Profile. The annotated code follows.

Note: In the following listing, the validations at lines 8 and 12-14 are redundant. They occur as part of the validation at line 16 and are present to gain finer control over bug detection. If the validation at line 16 fails, we know something is wrong with the Profile. Lines 8 and 12-14 tell us specifically which property set failed validation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private void validateCurrState( Profile expState )
{
    Profile actState    = new Profile();
    
    actState.setName( testGUI.getName() );
    actState.setGridUnit( testGUI.getGridUnit() );
    collectGraphProperties( actState );
    assertEquals( expState.getMainWindow(), actState.getMainWindow() );
    Stream.of( allSetNames )
        .forEach( s -> {
            collectLineProperties( s, actState );
            LinePropertySet expSet  = expState.getLinePropertySet( s );
            LinePropertySet actSet  = actState.getLinePropertySet( s );
            assertEquals( expSet, actSet, s );
        });
    assertEquals( expState, actState );
}
  • Line 3: Create a new Profile. We will use it to collect the actual values of all GUI components related to Profile property editing.
  • Lines 5,6: Get the actual values of the components for editing the Profile name and grid unit.
  • Line 7: Collect the actual values of components that edit properties in the Profile’s GraphPropertySet.
  • Line 8: Verify that the expected values of the GraphPropertySet components match the actual values.
  • Lines 9-15: Collect and validate the actual values of the components that edit the four LinePropertySet subclasses:
    • Line 10: For each LinePropertySet subclass:
      • Line 11: Collect the actual values of components that edit properties of the given LinePropertySet subclass.
      • Line 12: Get the expected values of the components.
      • Line 13: Get the actual values of the components.
      • Line 14: Verify the actual values against the expected values. If the assertion fails, the third argument passed to the assertion will add the name of the responsible property to the JUnit output.
  • Line 16: Verify the actual values of all GUI components against their expected values.

ProfileEditorTest Principal Test Methods

Here is a discussion of the principal test methods of the ProfileEditorTest class.

⬛ public void testProfileEditor()
This method verifies that, after instantiation, the ProfileEditor is in the expected state. Here’s the code,

@Test
public void testProfileEditor()
{
    validateCurrState( baseProfile );
}

⬛ public void testGetFeedBack()
The testGetFeedback method verifies that the getFeedback method in the ProfileEditor returns a reasonable value. The code looks like this.

@Test
public void testGetFeedBack()
{
    JComponent  feedback    = testGUI.getFeedback();
    assertNotNull( feedback );
}

⏹ public void testApply()
This method verifies that the ProfileEditor’s apply facility updates the PropertyManager correctly. The annotated code follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void testApply()
{
    validateCurrState( baseProfile );
    applyDistinctProperties();
    validateCurrState( distinctProfile );
    testGUI.apply();
    Profile profile = new Profile();
    validateCurrState( profile );
}
  • Line 4: Sanity check; verify that the GUI is in its initial state.
  • Line 5: Change the values of all the relevant components.
  • Line 6: Sanity check; verify that GUI components have the expected values.
  • Line 7: Apply the new values to the PropertyManager.
  • Line 8: Instantiate a Profile with property values initialized from the PropertyManager.
  • Line 9: Verify that the PropertyManager agrees with the expected Profile property values.

⏹ public void testReset()
This method verifies that the ProfileEditor’s reset facility correctly restores the values of the relevant GUI components to those stored by the PropertyManager. The annotated code follows.

1
2
3
4
5
6
7
8
9
@Test
public void testReset()
{
    validateCurrState( baseProfile );
    applyDistinctProperties();
    validateCurrState( distinctProfile );
    testGUI.reset();
    validateCurrState( baseProfile );
}
  • Line 4: Sanity check; verify that the GUI is in its initial state.
  • Line 5: Change the values of all the relevant components.
  • Line 6: Sanity check; verify that GUI components have the expected values.
  • Line 7: Invoke the ProfileEditor’s reset facility, restoring the GUI to its original state.
  • Line 8: Verify that GUI components have their original values.

Summary

On this page, we implemented a JUnit test for the ProfileEditor. As part of this task, we wrote the support class ProfileEditorTestGUI, which extends the abstract class ProfileEditorTestBase. Our next task is to implement a dialog to contain the ProfileEditor.

Next: The ProfileEditorDialog