Digression: ProfileFileManager JUnit Test Development

This page isn't about JUnit test development specifically. It's more about developing facilities to support JUnit test methods for the ProfileFileManagerTest class. The discussion below assumes that you have read the first part of the Cartesian Plane Lesson 18 Page 7: ProfileFileManager JUnitTest page. Any code you find here should be considered pseudocode. Do not expect to find anything on this page checked into the GitHub respository.

To best understand the pseudocode, you should be familiar with the methods discussed on the above referenced page prior to the link to this page. In addition to those, we will be referencing the following two methods. See the discussion below for additional details.

private Thread executeChooserOp( Runnable runner ) private Thread executeNonChooserOp( Runnable runner )
1
2
3
4
5
6
7
8
9
private Thread executeChooserOp( Runnable runner )
{
    Thread  thread  = new Thread( runner );
    thread.start();
    Utils.pause( 250 );
    getFileChooserComponents();
    assertTrue( fileChooser.isVisible() );
    return thread;
}
1
2
3
4
5
6
7
8
9
private Thread executeNonChooserOp( Runnable runner )
{
    Thread  thread  = new Thread( runner );
    thread.start();

    
    
    return thread;
}
  • Line 1: These two methods are called when we want to execute a file manager operation. Use executeChooserOp if the operation requires the file chooser to be posted, otherwise, use executeNonChooserOp. The parameter is a Runnable the executes the operation, for example:
    () -> adHocResult = fileMgr.save( profile, adHocFile )
  • Lines 3,4: Start the given operation in a dedicated thread. We've seen this strategy before; the thread will be returned to the client which can then join it, pausing main thread execution until the dedicated thread completes.
  • Lines 5-7: If necessary, give the file chooser time to post, then collect the file chooser components (the text field and the save button) and verify that the chooser is visible.
  • Line 8: Return a reference to the dedicated thread to the client.

Here are two test methods that save a Profile to a file. The one on the right saves a given Profile to a file of the operator's choice; The one on the left requires no operator interaction. Let's first discuss the logic behind the tests.

save( Profile profile, File file ) saveAs( Profile profile )
 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
@Test
public void testSaveProfileFileGoRight()
{
    adHocFile.delete();
    // sanity check
    assertFalse( adHocFile.exists() );

    int     expAction   = fileMgr.getLastAction();
    Profile profile     = new Profile();
    profile.setName( "testSaveProfileFileGoRight" );
    
    Thread  thread  = executeNonChooserOp( 
        () -> adHocResult = fileMgr.save( profile, adHocFile )
    );


    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    assertTrue( adHocResult );
    assertTrue( adHocFile.exists() );
    File    currFile    = fileMgr.getCurrFile();
    int     lastAction  = fileMgr.getLastAction();
    assertTrue( compareFiles( adHocFile, currFile ) );
    assertTrue( fileMgr.getLastResult() );
    assertEquals( expAction, lastAction );

    thread  = executeNonChooserOp( 
        () -> adHocProfile = fileMgr.open( adHocFile )
    );
    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    assertEquals( profile, adHocProfile );
    assertTrue( compareFiles( adHocFile, currFile ) );
    assertTrue( fileMgr.getLastResult() );
    assertEquals( expAction, lastAction );
}
 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
@Test
public void testSaveAsProfileGoRight()
{
    adHocFile.delete();
    // sanity check
    assertFalse( adHocFile.exists() );


    Profile profile     = new Profile();
    profile.setName( "testSaveAsProfileGoRight" );
    
    Thread  thread  = executeChooserOp( 
        () -> adHocResult = fileMgr.saveAs( profile )
    );
    enterPath( adHocName );
    clickOn( saveButton );
    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    assertTrue( adHocResult );
    assertTrue( adHocFile.exists() );
    File    currFile    = fileMgr.getCurrFile();
    int     lastAction  = fileMgr.getLastAction();
    assertTrue( compareFiles( adHocFile, currFile ) );
    assertTrue( fileMgr.getLastResult() );
    assertEquals( ProfileFileManager.APPROVE, lastAction );

    thread  = executeNonChooserOp( 
        () -> adHocProfile = fileMgr.open( adHocFile )
    );
    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    assertEquals( profile, adHocProfile );
    assertTrue( compareFiles( adHocFile, currFile ) );
    assertTrue( fileMgr.getLastResult() );
    assertEquals( ProfileFileManager.APPROVE, lastAction );
}
  • Lines 4-6: At the conclusion of the test, we expect to find a file containing Profile properties. At the beginning of the test make sure the file doesn't exist; at the end, one of our validations will be to assert that the file exists.
  • Line 8: The method that saves to an explicit file requires no interaction with the file chooser, so after the operation completes we will expect the lastAction property to be unchanged. On this line we save the current value of lastAction so that at the end of the test we can verify that it hasn't changed.
  • Lines 9,10: Instantiate the Profile we want to save; give it at least one unique property so that, after the conclusion of the operation, we can verify that the given Profile is correctly saved to the given file.

    Caveat: At the conclusion of the saveAs operation we expect the lastAction property to be SAVE. But, if it is, we have no way of knowing whether the operation set it to SAVE, or it was SAVE at the start of the operation. We will address this issue below.

  • Lines 12-14: Initiate the given operation in a dedicated thread.
  • Lines 15,16: The operation that requires operator interaction via the file chooser has to enter a file name into the chooser's text field and push its Save button.
  • Line 17: Both operations potentially raise I/O errors. We're not expecting one, so check for an error dialog and assert that there isn't one.
  • Line 18: Wait for the operation to complete.
  • Line 19: Validate the operation's return value.
  • Line 20: Verify that the file has been created.
  • Lines 21-25: Validate the ProfileFileManager's lastAction and currfile properties. For the operation that uses the file chooser the expected value of lastAction is APPROVE; for the operation requiring no operator interaction, the expected value is whatever it was at the start of the operation
  • Lines 27-35: Read the saved file and verify its contents.
    • Lines 27-29: Read the saved file.
    • Line 30: Verify that no error dialog was posted.
    • Line 31: Wait for the operation to complete.
    • Lines 32: Verify the contents of the Profile.
    • Lines 33-35: Verify the file manager's currFile, lastResult, and lastAction properties.

A quick look at the above comparison convinces me that a lot of duplicate code can be encapsulated in helper methods. The first thing that captures my attention is the last three lines of both methods, and lines 23-25 of testSaveProfileFileGoRight. This logic should be executed not just at the end of every test, but the end of every operation. Let's write the following method to perform those validations:

private void 
validateState( File expFile, boolean expResult, int expAction )
{
    File    currFile    = fileMgr.getCurrFile();
    assertTrue( compareFiles( expFile, currFile ) );
    assertEquals( expResult, fileMgr.getLastResult() );
    assertEquals( expAction, fileMgr.getLastAction() );
}

My second observation is that almost every output test will conclude with verifying the output, as with lines 27-35 above. This validation can be encapsulated in a method that reads a given file and compares it to a given Profile, such as the following:

private void 
validateFile( Profile expProfile, File file )
{
    verify the given file exists
    int expAction = fileMgr.getLastAction();
    Profile profile = new Profile();
    read the given file without going through the file chooser:
        () -> fileMgr.open( file, profile );
    assertEquals( expProfile, profile );
    validateState( file, true, expAction );
}

Next, consider that our test cases are nearly identical except for interaction with the file chooser. We can write a method that performs a save operation with a simple conditional that account for the requirement to use the file chooser. Consider this strategy:

 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
/*
    private void saveGoRight( 
        BooleanSupplier supplier, 
        File file, 
        boolean expectChooser
    )
{
    delete the target file
    expAction = fileMgr.getLastAction()
    formulate the Runnable that will drive a dedicated thread:
        Runnable    runner      = 
            () -> adHocResult = supplier.getAsBoolean();
    create and start the thread
    pause to allow file chooser to post (if necessary)
    if ( expectChooser )
    {
        find the file chooser's text field
        enter the file name into the text field
        click on the save button
        expAction = APPROVE
    }
    verify an error dialog was not posted
    join the thread
    verify the value of adHocResult
    verify the target file exists
    validateState( file, true, expAction );
}
*/
  • Line 3: The supplier parameter encapsulates the output operation to be executed, for example () -> fileMgr.save(profile, adHocFile) or () -> fileMgr.saveAs(profile).
  • Line 4: The file parameter refers to the file to be operated on, ahdHocFile in the original code.
  • Parameter expectChooser will be true if the operation under test requires operator interaction with the file chooser.
  • Line 8: Corresponds to lines 4-6 in the original code.
  • Line 9: Sets the "expected action" to the current value of the file manager's lastAction property. If this operation requires operator interaction with the file chooser, it will be updated at line 20.
  • Lines 10-14: Correspond to lines 12-14 in the original code.
  • Lines 15-21: This code replaces lines 15 and 16 in the original code.
  • Lines 22-26: Correspond to lines 30-35 in the original code.

My last thought regarding code encapsulation is that we have a lot of complications surrounding the file manager state (see Caveat, above), such as:

Example 1:

  • Start with the last-action property = APPROVE;
  • Begin execution of an operation that requires operator interaction with the file chooser;
  • Cancel the operation via the file chooser;
  • Verify that last-action is now CANCEL.

Example 2:

  • Start with the last-result property = false;
  • Begin execution of an operation that requires operator interaction with the file chooser;
  • Cancel the operation via the file chooser;
  • Verify that last-result is still false.

To help with state issues, let's ensure that, at the conclusion of our BeforeEach method, our test environment state will be very specific state. Let's also have methods that we can use to set the state of the last-action and last-result properties. The last two methods are necessary because setting the last-action and last-result states is non-trivial; they require executing operations that will achieve the desired result:

🟦public void beforeEach()
This method has the following postconditions:

  • The PropertyManager state will be in its original state;
  • Set all the fields associated with file chooser components (chooserName, openButton, etc.) will be null;
  • The physical adHocFile will not exist; all other files will be unchanged from there initial states.
  • The last-result property will be false;
  • The last-action property will be CANCEL;

🟦private void setLastAction( int action )
This method initiates an input operation that requires operator interaction with the file chooser. It dismisses the file chooser dialog with the given action (APPROVE or CANCEL).

🟦private void setLastResult( boolean expResult )
This method will initiate an input operation that does not require interaction with the file chooser. Depending on the value of expResult, the input operation will be one that completes with an I/O error or one that completes with no error.

With the implementation of the above facilities, our tests for save(Profile, File) and saveAs(Profile) look like this:

save( Profile profile, File file ) saveAs( Profile profile )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void testSaveProfileFileGoRight()
{
    Profile          profile     = new Profile();
    profile.setName( "testSaveProfileFileGoRight" );
    BooleanSupplier  supplier    = 
        () -> fileMgr.save( profile, adHocFile );
    saveGoRight( supplier, adHocFile, false );
    validateFile( profile, adHocFile );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void testSaveAsProfileGoRight()
{
    Profile         profile     = new Profile();
    profile.setName( "testSaveAsProfile" );
    BooleanSupplier supplier    = 
        () -> fileMgr.saveAs( profile );
    saveGoRight( supplier, adHocFile, true );
    validateFile( profile, adHocFile );
}

Next, let's look at possible implementations for input operations. In the following figure, the code on the left is a possible test method for ProfileFileManager.open(), and the one on the left for ProfileFileManager.open(File). The first requires operator interaction with the file chooser, the second does not.

open() open( File file )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
public void testOpenGoRight()
{
    int expAction   = fileMgr.getLastAction();
    Thread  thread  = executeChooserOp( 
        () -> adHocProfile = fileMgr.open()
    );
    enterPath( distinctName );
    clickOn( openButton );
    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    assertEquals( distinctProfile, adHocProfile );
    validateState( distinctFile, true, expAction );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
public void testOpenFileGoRight()
{
    
    Thread  thread  = executeNonChooserOp( 
        () -> adHocProfile = fileMgr.open( distinctFile )
    );

    
    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    assertEquals( distinctProfile, adHocProfile );
    validateState( distinctFile, true, ProfileFileManager.APPROVE );
}

As with the save( Profile profile, File file ) and saveAs( Profile profile ) test methods, the only difference between these two methods is how we handle file chooser interaction at line 4, where we anticipate (if necessary) the expected value of the last-action property at the end of the test, and lines 8 and 9 where we configure the file chooser and press its Save button. Taking that into account, we can implement a helper method that encapsulates all the duplicate code:

 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 openGoRight(
    Supplier<Profile> supplier, 
    File file, 
    boolean expectChooser
)
{
    verify the target file exists
    expAction = fileMgr.getLastAction()
    formulate the Runnable that will drive a dedicated thread:
        Runnable runner = () -> adHocProfile = supplier.get();
    create and start the thread
    pause to allow file chooser to post (if necessary)
    if ( expectChooser )
    {
        find the file chooser's text field
        enter the file name into the text field
        click on the open button
        expAction = APPROVE
    }
    verify an error dialog was not posted
    join the thread
    verify the value of adHocResult
    validateState( file, true, expAction );
}
*/

Re-writing our test methods to take advantage of the above, we get:

open() open( File file )
1
2
3
4
5
6
7
8
@Test
public void testOpenGoRight()
{
    Supplier<Profile>   supplier    = 
        () -> fileMgr.open();
    openGoRight( supplier, distinctFile, true );
    assertEquals( distinctProfile, adHocProfile );
}
1
2
3
4
5
6
7
8
@Test
public void testOpenFileGoRight()
{
    Supplier<Profile>   supplier    = 
        () -> fileMgr.open( distinctFile );
    openGoRight( supplier, distinctFile, false );
    assertEquals( distinctProfile, adHocProfile );
}

Let's look at a couple of go-wrong tests, executing a save or open operation that results in an I/O error. On the left side of the figure is a go-wrong test that requires operation interaction with the file chooser, on the right is a test that does not require operator intervention.

saveAs( Profile profile ) save( Profile profile, File file )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
public void testSaveAsProfileGoWrong()
{
    assertTrue( readOnlyFile.exists() );
    assertFalse( readOnlyFile.canWrite() );

    Thread  thread  = executeChooserOp( 
        () -> adHocResult = fileMgr.saveAs( adHocProfile )
    );
    enterPath( readOnlyName );
    clickOn( saveButton );
    assertTrue( dismissErrorDialog() );
    Utils.join( thread );
    validateState( null, false, ProfileFileManager.APPROVE );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
public void testSaveProfileFileGoWrong()
{
    assertTrue( readOnlyFile.exists() );
    assertFalse( readOnlyFile.canWrite() );
    int expAction   = fileMgr.getLastAction();
    Thread  thread  = executeNonChooserOp( 
        () -> fileMgr.save( new Profile(), readOnlyFile )
    );
    
    
    assertTrue( dismissErrorDialog() );
    Utils.join( thread );
    validateState( null, false, expAction );
}
open() open( File file )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
public void testOpenGoWrong()
{

    Thread  thread  = executeChooserOp( 
        () -> adHocProfile = fileMgr.open()
    );
    enterPath( noSuchFileName );
    clickOn( openButton );
    assertTrue( dismissErrorDialog() );
    Utils.join( thread );
    assertNull( adHocProfile );
    validateState( null, false, ProfileFileManager.APPROVE );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
public void testOpenFileGoWrong()
{
    int     expAction   = fileMgr.getLastAction();
    Thread  thread      = executeChooserOp( 
        () -> adHocProfile = fileMgr.open( noSuchFile )
    );


    assertTrue( dismissErrorDialog() );
    Utils.join( thread );
    assertNull( adHocProfile );
    validateState( null, false, expAction );
}

As we did with the go-right helper methods, we can encapsulate common code in save-go-wrong and open-go-wrong methods, as seen here:

 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 saveGoWrong( 
    BooleanSupplier supplier, 
    File file, 
    boolean expectChooser
)
{
    expAction = fileMgr.getLastAction()
    formulate the Runnable that will drive a dedicated thread:
        Runnable runner = 
            () -> adHocResult = supplier.getAsBoolean();
    pause to allow file chooser to post (if necessary)
    if ( expectChooser )
    {
        find the file chooser's text field
        enter the file name into the text field
        click on the save button
        expAction = APPROVE
    }
    verify an error dialog was posted
    join the thread
    verify the value of adHocResult
    validateState( file, true, expAction );
}
*/
 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 openGoWrong(
    Supplier<Profile> supplier, 
    File file, 
    boolean expectChooser
)
{
    expAction = fileMgr.getLastAction()
    formulate the Runnable that will drive a dedicated thread:
        Runnable runner = 
            () -> adHocProfile = supplier.get()
    pause to allow file chooser to post (if necessary)
    if ( expectChooser )
    {
        find the file chooser's text field
        enter the file name into the text field
        click on the open button
        expAction = APPROVE
    }
    verify an error dialog was posted
    join the thread
    verify the value of adHocProfile
    validateState( file, true, expAction );
}
*/

And then we can rewrite our go-wrong test methods as shown below.

open() open( File file )
1
2
3
4
5
6
7
@Test
public void testOpenGoWrong()
{
    Supplier<Profile>   supplier    = 
        () -> fileMgr.open();
    openGoWrong( supplier, noSuchFile, true );
}
1
2
3
4
5
6
7
@Test
public void testOpenFileGoWrong()
{
    Supplier<Profile>   supplier    = 
        () -> fileMgr.open( noSuchFile );
    openGoWrong( supplier, distinctFile, false );
}

One more type of test can be encapsulated: file manager operations that are canceled via the file chooser dialog. The encapsulation is easy because we don't need different methods for input and output operations. Below are two figures; the first shows the pseudocode for a method that encapsulates the cancel logic, and the second presents one input cancel test and one output cancel test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/*
private void cancelOperation(
    Supplier<?> supplier, 
    File file
)
{
    expFile = fileMgr.getCurrFile();
    expLastResult = fileMgr.getLastResult()
    formulate the Runnable that will drive a dedicated thread:
        Runnable    runner      = () -> supplier.get() 
    start dedicate thread to encapsulate the operation
    
    pause to allow file chooser to post
    find the file chooser's text field
    enter the file name into the text field
    click on the cancel button

    join the thread
    validateState( expFile, expResult, CANCEL )
}
*/
  • Line 3: This time we declare out supplier with a wildcard (?) because we're using it for both input (Supplier<Profile> required) and output (Supplier<Boolean> required) operations.
  • Lines 7,8: Record the values of the current-file and last-result properties for validation at the end of the operation (canceled operation do not change these properties).
  • Lines 9-11: Start the operation in a dedicated thread.
  • Lines 13-16: There is not "expect file chooser" logic this time; cancel operation always go through the file chooser. Wait for the file chooser to post, enter the file name and push the Cancel button.
  • Line 18: Wait for the dedicated thread to complete.
  • Line 19: Verify that the curr-file and last-result properties are unchanged, and the last-action property is set to CANCEL.

Following are the revised test methods.

open() saveAs()
1
2
3
4
5
6
@Test
public void testOpenCancel()
{
    Supplier<?> supplier    = () -> fileMgr.open();
    cancelOperation( supplier, baseFile );
}
1
2
3
4
5
6
@Test
public void testSaveAsCancel()
{
    Supplier<?> supplier    = () -> fileMgr.saveAs();
    cancelOperation( supplier, adHocFile);
}

Final Thoughts

We can take this strategy of encapsulating duplicate logic further, if we wanted to. We can imagine a method, for example, capable of handling any open operation:

private void executeOpen(
    Supplier<Profile> supplier, 
    File file, 
    boolean expectChooser,
    boolean cancel,
    boolean expectError
)

While I am not, in principle, opposed to a method like this, I think it would get awfully busy. A better approach might be to declare a nested class with a state and factory methods such as this:

private class Operation
{
    private final Supplier<Profile> reader;
    private final BooleanSupplier   writer;
    private final boolean           requiresChooer;
    private final boolean           expectError;
    private final boolean           mustCancel;
    
    public Operation getInputGoRightTest( 
        Supplier<Profile> supplier,
        boolean requiresChooser
    ) 
    { ... }
    
    public Operation getOutputGoWrongTest( 
        BooleanSupplier supplier,
        boolean requiresChooser
    ) 
    { ... }
    
    public Operation getInputCancelTest( 
        Supplier<Profile> supplier
    ) 
    { ... }
    
    public void execute() { ... }
}

We could then divide the execution logic between helper methods in the nested class.