Cartesian Plane Lesson 18 Page 7: ProfileFileManager JUnitTest

Profile, ProfileFileManager, File I/O, JUnit

On the previous page, we implemented ProfileFileManager, a class to encapsulate file I/O operations for reading and writing Profile data. On this page, we will implement a JUnit test for ProfileFileManager. We won’t need a separate test class to manage the GUI because the only GUI interaction is with the JFileChooser, posted when we want the operator to select a file and an error dialog posted in response to an I/O error. As we will ultimately discover, however, the ProfileFileManager and the ProfileEditorDialog JUnit tests overlap significantly in the need for test data generation and management. We will, therefore, have a separate utility class, ProfileFileManagerTestData, to perform these tasks. We’ll begin our discussion with the ProfileFileManagerTestData class.

GitHub repository: Cartesian Plane Lesson 18

Previous Lesson: Cartesian Plane Lesson 18 Page 6: ProfileFileManager

The ProfileFileManagerTestData class

Note: I originally built this functionality directly into the ProfileFileManagerTest class. Later, when I realized I would need the same functionality for the ProfileEditorDialogTest class, I moved it to this utility class.

This class is responsible for creating and managing the data files for testing associated with the ProfileFileManager class.  As determined by the file utilities in the Utils class, all test files will be stored in the ProfileFileManagerTest subdirectory of the project test directory. The subdirectory will contain the following files:

  • BaseProfile.txt
    This file will contain the default property values in the PropertyManager class. Its content should not be changed after initialization.
  • DistinctProfile.txt
    This file will contain property values different from those in BaseProfile.txt. Its content should not be changed after initialization.
  • ReadonlyFile.txt
    This file will be created with the same data as BaseProfile. After creation, it will be made unwritable (but still readable). It is for use in testing output operations that result in I/O exceptions.
  • NoSuchFile.txt
    This file will not be physically created. It is for use in testing input operations that result in I/O exceptions.
  • AdHocFile.txt
    This facility will not physically create this file. The client can create, populate, and delete it as needed.

ProfileFileManagerTestData Class Infrastructure

The following sections discuss the ProfileFileManagerTestData class’s fields, static initializer, and helper methods.

⬛ Fields
Below, find an annotated listing of the ProfileFileManagerTestData fields.

 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
public class ProfileFileManagerTestData
{
    private static final String     testDataDirName = 
        "ProfileFileManagerTest";
    private static final  File      testDataDir     = 
        Utils.getTestDataDir( testDataDirName );
    private static final  String    baseName        = 
        "BaseProfile.txt";
    private static final  String    distinctName    = 
        "DistinctProfile.txt";
    private static final  String    adHocName       = 
        "AdHoc.txt";
    private static final  String    readOnlyName    = 
        "ReadonlyFile.txt";
    private static final  String    noSuchFileName  = 
        "NoSuchFile.txt";
    private static final File       baseFile        = 
        new File( testDataDir, baseName );
    private static final File       distinctFile    = 
        new File( testDataDir, distinctName );
    private static final File       adHocFile       = 
        new File( testDataDir, adHocName );
    private static final File       readOnlyFile    = 
        new File( testDataDir, readOnlyName );
    private static final File       noSuchFile      = 
        new File( testDataDir, noSuchFileName );
    
    private static final Profile    baseProfile     = new Profile();
    private static final Profile    distinctProfile = 
        ProfileUtils.getDistinctProfile( baseProfile );
    // ...
}
  • Lines 3,4: The name of the subdirectory for ProfileFileManager test data.
  • Lines 5,6: The File object that encapsulates the test directory.
  • Lines 7-16: The names of the test data files.
  • Lines 17-26: The File objects encapsulating the test data files.
  • Line 28: The Profile from which the baseFile is populated.
  • Lines 29,30: The Profile from which the distinctFile is populated.

⬛ Private Methods
The ProfileFileManagerTestData class has the following private methods.

🟦 private static Profile getProfile( File file ) throws IOException
🟦 private static void saveProfile( Profile profile, File file ) throws IOException
These methods read and write a Profile from/to a given File. They do not go through the ProfileFileManager, which has two advantages:

  • If the file manager has a bug in it, the bug will not affect our test code.
  • The test code in this class cannot affect the state of the file manager under test.

Here’s the code for these two methods.

 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
private static Profile getProfile( File file )
    throws IOException
{
    Profile profile = new Profile();
    try ( 
        FileReader fReader = new FileReader( file );
        BufferedReader bReader = new BufferedReader( fReader );
    )
    {
        Stream<String>  lines = bReader.lines();
        ProfileParser   parser  = new ProfileParser( profile );
        parser.loadProperties( lines );
    }
    return profile;
}

private static void saveProfile( Profile profile, File file ) 
    throws IOException
{
    ProfileParser   parser      = new ProfileParser( profile );
    try ( 
        FileOutputStream fileStr = new FileOutputStream( file );
        PrintWriter writer = new PrintWriter( fileStr );
    )
    {
        parser.getProperties()
            .forEach( writer::println );
    }
}

Let’s quickly review the try with resources statement, examples of which can be seen in lines 5-8 and 21-24 of the above code.

  • A try with resources statement adds parentheses to the try keyword.
  • Variables (resources) declared inside the parentheses must implement the Closeable interface.
  • Resources declared in the try statement will automatically be closed after the try block terminates, whether it completes normally or throws an exception.
  • A catch block is optional with a try with resources statement. The above code does not declare catch blocks, so the methods must declare that they throw IOException.

ProfileFileManagerTestData Class Static Initialization Block
As a utility class, ProfileFileManagerTestData has no public constructor. It has a private constructor, which does nothing except prevent the class from being instantiated. static initialization block is executed once, when the class is loaded, to complete field initialization. A static initialization block is declared directly in the body of the class. As shown below, it consists of the static keyword followed by a block statement:
    public class Abc
    {
        // ...
        static
        {

            // ...
        }
    }

Static initialization blocks are particularly helpful for initializing final class fields. Here is the annotated static initialization block for the ProfileFileManagerTestData class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static
{
    try
    {
        Utils.recursiveDelete( testDataDir );
        testDataDir.mkdirs();
        
        saveProfile( baseProfile, baseFile );
        saveProfile( distinctProfile, distinctFile );
        saveProfile( baseProfile, readOnlyFile );
        readOnlyFile.setWritable( false );
    }
    catch ( IOException exc )
    {
        exc.printStackTrace();
        System.exit( 1 );
    }
}
  • Line 1: Begin static initialization block.
  • Lines 2,18: Curly brackets that delimit the static initialization block.
  • Lines 3-12: Try block, which is necessary because saveProfile can throw an IOException.
    • Line 5: Clean up test data that might be left over after the last test execution. The data subdirectory we’re trying to delete might not exist, which is fine.
    • Line 6: Create the test data subdirectory.
    • Lines 8-10: Create the baseFile, distinctFile, and readOnlyFile data files.
    • Line 11: Remove write permissions from readOnlyFile.
  • Lines 13-17: This is the catch block for IOException. If this code throws an IOException, the test cannot continue, so we print a stack trace and exit from the test.

ProfileFileManagerTestData Class Constructor
As a utility class, ProfileFileManagerTestData does not have a public constructor. It has a private default constructor that does nothing but prevent instantiation. It looks like this:
    private ProfileFileManagerTestData()
    {
    }

ProfileFileManagerTestData Public Methods
The ProfileFileManagerTestData class has the following public methods:

🟦 public static Profile getBaseProfile()
🟦 public static Profile getDistinctProfile()
These methods return the Profiles used to populate the baseFile and the distinctFile. Here’s the code.

public static Profile getBaseProfile()
{
    return baseProfile;
}
public static Profile getDistinctProfile()
{
    return distinctProfile;
}

🟦 public static File getTestDataDir()
🟦 public static File getBaseFile()
🟦 public static File getDistinctFile()
🟦 public static File getAdhocFile()
🟦 public static File getReadonlyFile()
🟦 public static File getNosuchFile()
The getTestDataDir method is the getter for the File reference to the ProfileFileManager test data subdirectory, testDataDir. The others are the getters for the File references to the various test data files. There’s nothing special about them; you can find them in the GitHub repository.

Note: Rather than having getters for these resources, there’s no reason why we couldn’t make them public. If we do, convention requires changing their names to consist of only uppercase characters and underscores, BASE_FILE, DISTINCT_FILE, etc. The only advantage to that strategy is not having to write and document the getters.

🟦 public static boolean compareFileNames( File file1, File file2 )
This method takes two Files as input and returns true if they encapsulate the same test file; if both Files are null, we return true. See accompanying note.

Note: If we want to determine whether ProfileFileManager.getCurrFile() returns the correct value, we cannot compare the returned value to the expected File. For example:
fileManger.saveAs( profile )
enter adHocName into file chooser dialog
push Save button
assertEquals( adHocFile, fileManger.getCurrFile() )

// the above assertion may erroneously fail
To see why this is, look at the documentation for File.equals, which says:

… Returns true if and only if the argument is not null and is an abstract pathname that is the same as this abstract pathname. Whether or not two abstract pathnames are equal depends on the underlying operating system

The reason given is that pathnames are case-sensitive on some operating systems and not on others. So, the compareFileNames method extracts the names from the File objects, converts them to identical case, and then compares them. This is not a perfect solution because, on MS Windows, for example, ADHOCFile is the same file as adhocFile, but on MacOS, they are not. In our tests, however, no file names differ in case only, so this method of testing our file names for equality is sound.

Here’s the code for compareFileNames; at lines 4-8, we allow for one or both File objects to be null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static boolean compareFileNames( File file1, File file2 )
{
    boolean result  = false;
    if ( file1 == file2 )
        result = true;
    else if ( file1 == null || file2 == null )
        result = false;
    else
    {
        String  name1   = file1.getName().toUpperCase();
        String  name2   = file2.getName().toUpperCase();
        result  = name1.equals( name2 );
    }
    return result;
}

🟦 public static boolean validateFile( Profile expProfile, File file )
This method returns true if the given File exists and contains the given Profile. Here’s the code.

public static boolean validateFile( Profile expProfile, File file )
{
    Profile actProfile  = null;
    try
    {
        actProfile  = getProfile( file );
    }
    catch ( IOException exc )
    {
        String  msg =
            "\"" + file.getName() + "\" read failure";
        throw new ComponentException( msg, exc );
    }
    boolean result      = expProfile.equals( actProfile );
    return result;
}

🟦 public static void shutdown()
The purpose of the shutdown method is to release all resources and delete all files and directories associated with the ProfileFileManagerTestData class. The code looks like this.

public static void shutdown()
{
    Utils.recursiveDelete( testDataDir );
}

ProfileFileManagerTest Class Development Strategy

The requirements of the ProfileFileManager are laid out in detail on the previous page; see Save Operations, Open Operations, and Miscellaneous Facilities. We will need one test for each miscellaneous public method, but all the I/O methods will require multiple tests. For example, to thoroughly test the saveAs(Profile) method, we need:

  1. testSaveAsProfileGoRight()
    • Issue the command
    • Verify that the JFileChooser starts
    • Enter the path to a writable file
    • Push the dialog’s Save button
    • Verify that an error dialog is not posted
    • Verify the operative method returns true
    • Verify that the file is written
    • Verify that getCurrFile returns the correct file
    • Verify that getLastResult returns true
    • Verify that getLastAction returns APPROVE
  2. testSaveAsProfileGoWrong()
    • Issue the command
    • Verify that the JFileChooser starts
    • Enter the path to a file that is not writable
    • Push the dialog’s Save button
    • Verify that an error dialog is posted
    • Verify the operative method returns false
    • Verify that getCurrFile returns null
    • Verify that getLastResult returns false
    • Verify that getLastAction returns APPROVE
  3. testSaveAsProfileCancel()
    • Issue the command
    • Verify that the JFileChooser starts
    • Enter a pathname
    • Push the dialog’s Cancel button
    • Verify that an error dialog is not posted
    • Verify that the current-file property is unchanged
    • Verify that the last-result property is unchanged
    • Verify that getLastAction returns CANCEL

⬛ GUI Interaction
Our interaction with GUI components for this test is minimal, so we won’t have an auxiliary class to manage them. However, we will still take steps to ensure that GUI management is performed on the EDT. Every file operation, button push, and text entry will occur on the EDT.

⏹ Test Data
All test data is generated at the start of the test. As determined by the file utilities in the Utils class, all test files will be stored in a dedicated subdirectory of the project test directory. The subdirectory is deleted after all testing has been completed. There will be files containing predictable data, a file for ad hoc use, and files for testing input and output failure. Please have a look at The ProfileFileManagerTestData Class for details.

Implementation

Here, we will discuss the infrastructure and public facilities of our implementation.

Infrastructure

The following sections will discuss our test class’s fields, before/after methods, and private methods.

⬛ Fields
Below, find an annotated listing of the ProfileFileManagerTest fields.

 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
public class ProfileFileManagerTest
{
    private static final long       pauseInterval   = 125;
    private static final  File      testDataDir     = 
        ProfileFileManagerTestData.getTestDataDir();
    private static final File       baseFile        = 
        ProfileFileManagerTestData.getBaseFile();
    private static final File       distinctFile    = 
        ProfileFileManagerTestData.getDistinctFile();
    private static final File       adHocFile       = 
        ProfileFileManagerTestData.getAdhocFile();
    private static final File       readOnlyFile    = 
        ProfileFileManagerTestData.getReadonlyFile();
    private static final File       noSuchFile      = 
        ProfileFileManagerTestData.getNosuchFile();
    
    private static final Profile    baseProfile     = 
        ProfileFileManagerTestData.getBaseProfile();
    private static final Profile    distinctProfile = 
        ProfileFileManagerTestData.getDistinctProfile();
    private Profile                 adHocProfile    = new Profile();
    private boolean                 adHocResult     = false;
    private final ProfileFileManager    fileMgr     = 
        new ProfileFileManager();
    
    private final JFileChooser      fileChooser     = 
        fileMgr.getFileChooser();
    
    private JDialog         chooserDialog   = null;
    private JTextField      chooserName     = null;
    private JButton         openButton      = null;
    private JButton         saveButton      = null;
    private JButton         cancelButton    = null;

    private AbstractButton errorDialogOKButton;
    // ...
}
  • Line 3: The interval to pause when waiting for a dialog to post.
  • Lines 4-20: Invariant resources copied from ProfileFileManagerTestData. Declared here for convenience.
    • Lines 4,5: The test data subdirectory containing the test data files.
    • Lines 6-15: The test data files.
    • Lines 17-20: The Profiles used to create the baseFile and distinctFile data files.
  • Line 21: Profile for use as needed by test methods.
  • Line 22: This field contains the result of the last executed output operation.
  • Lines 23,24: The ProfileFileManager under test.
  • Lines 26,27: The JFileChooser from the ProfileFileManager under test.
  • Line 29: The dialog containing the file chooser. We don’t know precisely when this dialog is created; we only know with certainty that it exists after the first time we perform an operation requiring the operator to choose a file. So, we don’t try to initialize this field until after we begin an operation requiring operator interaction, such as open(Profile) or saveAs(Profile). See also getChooserDialog.
  • Lines 30-33: Component children of the JFileChooser and its dialog with which tests must interact. Because the JFileChooser configuration is somewhat dynamic; for example, sometimes there’s a Save button, and sometimes there’s not; references to these components are obtained every time they’re needed. See also getFileChooserComponents.
    • Line 30: The text field where a file name can be entered.
    • Line 31: The Open button to complete input operations.
    • Line 32: The Save button to complete output operations.
    • Line 33: The Cancel button.
  • Lines 35: The OK button from the dialog that displays I/O errors. We can’t be sure when the error dialog and its button exist, so we don’t try to initialize this field until after we expect the dialog to become visible. The field’s value is updated whenever we find a visible error dialog. See getErrorDialogAndOKButton and dismissErrorDialog.

⬛ Private Methods
Following is a description of the private methods contained in the ProfileFileManagerTest class.

🟦 private JDialog getChooserDialog()
This method obtains the dialog encapsulating the JFileChooser. Since we can’t be sure, in general, when the dialog is first created, we can’t call getChooserDialog from the class initialization logic. Instead, we execute it only after initiating an operation that requires interaction with the file chooser; see also getFileChooserComponents. Here is an annotated listing of this method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private JDialog getChooserDialog()
{
    final boolean           canBeFrame  = false;
    final boolean           canBeDialog = true;
    final boolean           mustBeVis   = true;
    final String            title       = fileChooser.getDialogTitle();
    final Predicate<Window> isDialog    = w -> (w instanceof JDialog);
    final Predicate<Window> hasTitle    = 
        w -> title.equals( ((JDialog)w).getTitle() );
    final Predicate<Window> pred        = isDialog.and( hasTitle );
    
    ComponentFinder     finder      = 
        new ComponentFinder( canBeDialog, canBeFrame, mustBeVis );
    Window          window  = finder.findWindow( pred );
    assertNotNull( window );
    assertTrue( window instanceof JDialog );
    return (JDialog)window;
}
  • Lines 3-5: Encapsulate the search constraints that we specify when instantiating ComponentFinder to search for a top-level window:
    • Line 3: The top-level window cannot be a JFrame.
    • Line 4: The top-level window can be a JDialog.
    • Line 5: The top-level window must be visible.
  • Line 6: Get the expected title of the target dialog.
  • Line 7: Formulate a Predicate that will evaluate to true when a top-level window is a JDialog.
  • Lines 8,9: Formulate a Predicate that will evaluate to true when a JDialog has the expected title.
  • Line 10: Formulate a Predicate that is the logical-and of the previous two.
  • Lines 12-14: Instantiate a ComponentFinder and search for the target dialog.
  • Line 15: Verify that we found a Window.
  • Line 16: Verify that the Window is a JDialog.
  • Line 17: Return the JDialog.

🟦 private void getFileChooserComponents()
This method gets the dialog that encapsulates the JFileChooser (if necessary) and the constituent components required for testing. It should only be called after we expect the dialog to be visible (see getChooserDialog()). Additionally, the state of the dialog is generally unpredictable; for example, sometimes the Save button exists, and sometimes it doesn’t. Therefore, this method must be called every time a reference to a component is needed. Following is an annotated listing of the getFileChooserComponents method.

 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
private void getFileChooserComponents()
{
    final Predicate<JComponent> namePred    =
        c -> (c instanceof JTextField );
    final Predicate<JComponent> openPred    =
        ComponentFinder.getButtonPredicate( "Open" );
    final Predicate<JComponent> savePred    =
        ComponentFinder.getButtonPredicate( "Save" );
    final Predicate<JComponent> cancelPred  =
        ComponentFinder.getButtonPredicate( "Cancel" );
    
    if ( chooserDialog == null )
        chooserDialog = getChooserDialog();
    
    Component   comp        = 
        ComponentFinder.find( fileChooser, namePred );
    assertNotNull( comp );
    assertTrue( comp instanceof JTextField );
    chooserName = (JTextField)comp;
    
    comp = ComponentFinder.find( fileChooser, cancelPred );
    assertNotNull( comp );
    assertTrue( comp instanceof JButton );
    cancelButton = (JButton)comp;
    
    openButton = null;
    comp = ComponentFinder.find( fileChooser, openPred );
    if ( comp != null )
    {
        assertTrue( comp instanceof JButton );
        openButton = (JButton)comp;
    }
    saveButton = null;
    comp = ComponentFinder.find( fileChooser, savePred );
    if ( comp != null )
    {
        assertTrue( comp instanceof JButton );
        saveButton = (JButton)comp;
    }
    assertTrue( openButton != null || saveButton != null );
}
  • Lines 3,4: Create a Predicate to locate the text field for entering a file name.
  • Lines 5-10: Create Predicates to locate the Save, Open, and Cancel buttons.
  • Lines 12,13: If necessary, find the JFileChooser dialog.
  • Lines 15-19: Find and validate the JTextField for entering a file name. This component is always expected to be present.
  • Lines 21-24: Find and validate the Cancel button, which is always expected to be present.
  • Line 26: Flag the Open button as not found.
  • Line 27: Attempt to find the Open button, which may or may not be present.
  • Lines 28-32: If present, validate the Open button.
  • Line 33: Flag the Save button as not found.
  • Line 34: Attempt to find the Save button, which may or may not be present.
  • Lines 35-39: If present, validate the Save button.
  • Line 40: Verify that at least one of the Open or Save buttons is present.

🟦 private void getErrorDialogAndOKButton()
This method searches for the error dialog and its OK button. It only searches for visible dialogs (see lines 5 and 8 in the listing below). If the dialog and OK button are found, the errorDialogOKButton field will reference the OK button. Otherwise, the field is set to null (lines 7 and 20).

There are times when we expect the error dialog not to be visible. For example:
    write a Profile to the adHoc file
    verify that an error dialog is not posted

On the other hand, if we find a dialog, we expect it to have an OK button; if we don’t see an OK button, we treat it as an error (lines 18 and 19). Here is an annotated listing of this method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void getErrorDialogAndOKButton()
{
    final boolean canBeFrame    = false;
    final boolean canBeDialog   = true;
    final boolean mustBeVis     = true;
    GUIUtils.schedEDTAndWait( () ->  {
        errorDialogOKButton = null;
        ComponentFinder finder  = 
            new ComponentFinder( canBeDialog, canBeFrame, mustBeVis );
        Window          window  = finder.findWindow( c -> true );
        if ( window != null )
        {
            assertTrue( window instanceof JDialog );
            Predicate<JComponent>   pred    = 
                ComponentFinder.getButtonPredicate( "OK" );
            JComponent              comp    = 
                ComponentFinder.find( window, pred );
            assertNotNull( comp );
            assertTrue( comp instanceof AbstractButton );
            errorDialogOKButton = (AbstractButton)comp;
        }
    });
}
  • Lines 3-5: Initialization parameters for instantiating a ComponentFinder to locate the error dialog. The encapsulating window cannot be a JFrame, can be a JDialog, and must be visible.
  • Lines 6-22: Execute on the EDT:
    • Line 7: Initialize the OK button to not found.
    • Lines 8,9: Attempt to locate the dialog, which may or may not be visible.
    • Lines 11-21: If a candidate top-level window is found:
      • Line 13: Verify that the window is a JDialog.
      • Lines 14-17: Find the dialog’s OK button.
      • Lines 18,19: Verify that a component is found and that it is an AbstractButton.
      • Line 20: Set the value of the OK button field.

🟦 private void validateState( File expFile, boolean expResult, int expAction )
This method verifies the file manager’s state. For example, after saving a Profile to adHocFile, a test case might wish to validate that getCurrFile returns adHocFile, getLastResult returns true, and getLastAction returns APPROVE. Invocation of validateState will be the last line of code in many of our test cases. Here’s the code.

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() );
}

🟦 private static void clickOn( AbstractButton button )
As shown below, this method clicks the given button in the context of the EDT.

private static void clickOn( AbstractButton button )
{
    GUIUtils.schedEDTAndWait( button::doClick );
}

🟦 private boolean dismissErrorDialog()
This method looks for a visible error dialog. If found, it pushes the dialog’s OK button and returns true. If not found, it returns false. Test methods that expect an error to be raised will invoke dismissErrorDialog like this:
    assertTrue( dismissErrorDialog() );
Test methods do not expect errors will invoke the method like this:
    assertFalse( dismissErrorDialog() );
Here’s the annotated code for dismissErrorDialog; also, see getErrorDialogAndOKButton.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private boolean dismissErrorDialog()
{
    boolean dismissed   = false;
    Utils.pause( 250 );
    getErrorDialogAndOKButton();
    if ( errorDialogOKButton != null )
    {
        dismissed = true;
        clickOn( errorDialogOKButton );
    }
    return dismissed;
}
  • Line 3: Declare the variable to hold the result.
  • Line 4: Give the error dialog time to be posted. We are typically going to invoke this method from the main thread while a file operation is being executed in a dedicated thread, for example:
        Thread thread = [execute saveAs to unwritable file in new thread]
        assertTrue( dismissErrorDialog() );
        Utils.join( thread );

    Line 4 pauses the main thread so the dedicated thread has time to execute the saveAs operation and post the error dialog.
  • Line 5: Look for the error dialog and its OK button. If the dialog can’t be found, errorDialogOKButton will be set to null; see getErrorDialogAndOKButton.
  • Lines 6-10: If a visible dialog was found, click its OK button and set the result to true.
  • Line 11: Return the result.

🟦 private void enterPath( String fileName )
The enterPath method takes the given file name and formulates an absolute path to the file within the test directory, such as in the following MS Windows and Unix examples:
    C:\test_dir\test_data\ProfileFileManagerTest\AdHoc.txt
    /test_dir/test_data/ProfileFileManagerTest/AdHoc.txt

It enters the path into the JFileChooser’s text field; it assumes that the JFileChooser dialog is visible. The code looks like this:

private void enterPath( String fileName )
{
    assertNotNull( chooserName );
    File    file    = new File( testDataDir, fileName );
    String  path    = file.getAbsolutePath();
    GUIUtils.schedEDTAndWait( () -> chooserName.setText( path ) );
}

Digression: ProfileFileManager Test Case Comparison
Before examining the remaining infrastructure details, it may be helpful to consider the rationale behind the chosen implementation strategy. This page discusses the rationale. The discussion assumes that you are familiar with the material up to this point. It contains no specific implementation details and can be skipped at the reader’s discretion.

🟦 private void openGoRight(Supplier<Profile> supplier, File file, boolean expectChooser )
This method executes an input operation on the given file and validates the result. The operation is expected to succeed; it is executed in a dedicated thread, and this method does not return until the thread expires. If expectChooser is true, it assumes that the given operation requires interaction with the file chooser; it will wait for the chooser dialog to become visible, enter the file name of the given file, and push the dialog’s Open button. Here’s the annotated 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
27
28
29
30
31
private void openGoRight(
    Supplier<Profile> supplier, 
    File file, 
    boolean expectChooser
)
{
    assertTrue( file.exists() );
    
    Runnable    edtRunner   = 
        () -> adHocProfile = supplier.get();
    Runnable    runner      = 
        () -> GUIUtils.schedEDTAndWait( edtRunner );
    String      name        = file.getName();
    int         expAction   = fileMgr.getLastAction();
    Thread      thread      = new Thread( runner );
    thread.start();

    if ( expectChooser )
    {
        Utils.pause( pauseInterval );
        getFileChooserComponents();
        assertTrue( fileChooser.isVisible() );
        enterPath( name );
        clickOn( openButton );
        expAction = ProfileFileManager.APPROVE;
    }
    
    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    validateState( file, true, expAction );
}
  • Line 2: This is the given operation, for example, () -> fileMgr.open( baseFile ).
  • Line 3: This is the given file.
  • Line 4: This parameter must be true if the operation requires interaction with the file chooser.
  • Line 7: Sanity check; the input file must exist for an input operation to be successful.
  • Lines 9-12: Formulate the Runnables to drive the dedicated thread. Our dedicated thread will initiate the operation, but the operation needs to be scheduled on the EDT. The edtRunner lambda encapsulates the file manager operation. The runner lambda encapsulates the logic that runs when the dedicated thread is started; it schedules the file manager operation to run on the EDT and waits for it to complete.
  • Line 13: Get the name of the given file; if necessary, it will be entered into the chooser dialog (see line 23).
  • Line 14: Anticipate the value of the file manager’s lastAction property after the operation completes. If expectChooser is false, the expected value will be unchanged from the start of the operation. Otherwise, it will be APPROVE. See also line 25.
  • Lines 15,16: Instantiate and start the dedicated thread.
  • Lines 18-26: If operator interaction with the file chooser is expected:
    • Line 20: Give the chooser dialog time to post.
    • Line 21: Update references to the chooser’s components.
    • Line 22: Verify that the chooser dialog is visible.
    • Lines 23,24: Enter the name of the given file into the chooser and push the dialog’s Open button.
    • Line 25: Update the expected action to APPROVE.
  • Line 28: Verify that an error dialog is not posted.
  • Line 29: Wait for the thread to expire.
  • Line 30: Verify the file manager’s expected state following operation completion.

🟦 private void openGoWrong(Supplier<Profile> supplier, File file, boolean expectChooser )
This method executes an input operation on the given file and validates the result. The operation is expected to fail with an I/O error. It is executed in a dedicated thread, and this method does not return until the thread expires. If expectChooser is true, it assumes that the given operation requires interaction with the file chooser; it will wait for the chooser dialog to become visible, enter the file name of the given file, and push the dialog’s Open button. Here’s the annotated 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
27
28
29
30
private void openGoWrong(
    Supplier<Profile> supplier, 
    File file, 
    boolean expectChooser
)
{
    Runnable    edtRunner   = 
        () -> adHocProfile = supplier.get();
    Runnable    runner      = 
        () -> GUIUtils.schedEDTAndWait( edtRunner );
    String      name        = file.getName();
    int         expAction   = fileMgr.getLastAction();
    Thread      thread      = new Thread( runner );
    thread.start();

    if ( expectChooser )
    {
        Utils.pause( pauseInterval );
        getFileChooserComponents();
        assertTrue( fileChooser.isVisible() );
        enterPath( name );
        clickOn( openButton );
        expAction = ProfileFileManager.APPROVE;
    }
    
    assertTrue( dismissErrorDialog() );
    Utils.join( thread );
    assertNull( adHocProfile );
    validateState( null, false, expAction );
}
  • Line 2: This is the given operation, for example, () -> fileMgr.open( noSuchFile ).
  • Line 3: This is the given file.
  • Line 4: This parameter must be true if the operation requires interaction with the file chooser.
  • Lines 7-10: Formulate the Runnables to drive the dedicated thread. Our dedicated thread will schedule the operation on the EDT and wait for it to complete.
  • Line 11: Get the name of the given file; if necessary, it will be entered into the chooser dialog (see line 19).
  • Line 12: Anticipate the value of the file manager’s lastAction property after the operation completes. If expectChooser is false, the expected value will be unchanged from the start of the operation. Otherwise, it will be APPROVE. See also line 23.
  • Lines 13,14: Instantiate and start the dedicated thread
  • Lines 16-24: If operator interaction with the file chooser is expected:
    • Line 18: Give the chooser dialog time to post.
    • Line 19: Update references to the chooser’s components.
    • Line 20: Verify that the chooser dialog is visible.
    • Lines 21,22: Enter the name of the given file into the chooser and push the dialog’s Open button.
    • Line 23: Update the expected action to APPROVE.
  • Line 26: Verify that an error dialog was posted.
  • Line 27: Wait for the thread to expire.
  • Line 28: Verify the return value of the operative method.
  • Line 29: Verify the file manager’s expected state following operation completion.

🟦 private void saveGoRight(BooleanSupplier supplier, File file, boolean expectChooser )
This method executes an output operation on the given file and validates the result. The operation is expected to succeed; it is executed in a dedicated thread, and this method does not return until the thread expires. If expectChooser is true, it assumes that the given operation requires interaction with the file chooser; it will wait for the chooser dialog to become visible, enter the file name of the given file, and push the dialog’s Save button. Here’s the annotated 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
27
28
29
30
31
32
33
34
private void saveGoRight( 
    BooleanSupplier supplier, 
    File file, 
    boolean expectChooser
)
{
    file.delete();
    assertFalse( file.exists() );
    
    Runnable    edtRunner   = 
        () -> adHocResult = supplier.getAsBoolean();
    Runnable    runner      = 
        () -> GUIUtils.schedEDTAndWait( edtRunner );
    String      name        = file.getName();
    int         expAction   = fileMgr.getLastAction();
    Thread      thread  = new Thread( runner );
    thread.start();
    
    if ( expectChooser )
    {
        Utils.pause( pauseInterval );
        getFileChooserComponents();
        assertTrue( fileChooser.isVisible() );
        enterPath( name );
        clickOn( saveButton );
        expAction = ProfileFileManager.APPROVE;
    }
    
    assertFalse( dismissErrorDialog() );
    Utils.join( thread );
    assertTrue( adHocResult );
    assertTrue( file.exists() );
    validateState( file, true, expAction );
}
  • Line 2: This is the given operation, for example, () -> fileMgr.save( adHocFile ).
  • Line 3: This is the given file.
  • Line 4: This parameter must be true if the operation requires interaction with the file chooser.
  • Line 7: Delete the target file. After the operation is complete, we will verify that a new file has been created; see line 32.
  • Line 8: Sanity check; verify that the file has been deleted.
  • Lines 10-13: Formulate the Runnables to drive the dedicated thread. Our dedicated thread will schedule the operation on the EDT and wait for it to complete.
  • Line 14: Get the name of the given file; if necessary, it will be entered into the chooser dialog (see line 24).
  • Line 15: Anticipate the value of the file manager’s lastAction property after the operation completes. If expectChooser is false, the expected value will be unchanged from the start of the operation. Otherwise, it will be APPROVE. See also line 26.
  • Lines 16,17: Instantiate and start the dedicated thread.
  • Lines 19-27: If operator interaction with the file chooser is expected:
    • Line 21: Give the chooser dialog time to post.
    • Line 22: Update references to the chooser’s components.
    • Line 23: Verify that the chooser dialog is visible.
    • Lines 24,25: Enter the name of the given file into the chooser and push the dialog’s Save button.
    • Line 26: Update the expected action to APPROVE.
  • Line 29: Verify that no error dialog was posted.
  • Line 30: Wait for the dedicated thread to expire.
  • Line 31: Verify the return value of the operative method.
  • Line 32: Verify that the target file exists.
  • Line 33: Verify the file manager’s expected state following operation completion.

🟦 private void saveGoWrong(BooleanSupplier supplier, File file, boolean expectChooser)
This method executes an output operation on the given file and validates the result. The operation is expected to fail with an I/O error; it is executed in a dedicated thread, and this method does not return until the thread expires. If expectChooser is true, it assumes that the given operation requires interaction with the file chooser; it will wait for the chooser dialog to become visible, enter the name of the given file, and push the dialog’s Save button. Here’s the annotated 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
27
28
29
30
31
private void saveGoWrong( 
    BooleanSupplier supplier, 
    File file, 
    boolean expectChooser
)
{
    Runnable    edtRunner   = 
        () -> adHocResult = supplier.getAsBoolean();
    Runnable    runner      = 
        () -> GUIUtils.schedEDTAndWait( edtRunner );
    String      name        = file.getName();
    int         expAction   = fileMgr.getLastAction();
    
    Thread      thread  = new Thread( runner );
    thread.start();
    
    if ( expectChooser )
    {
        Utils.pause( pauseInterval );
        getFileChooserComponents();
        assertTrue( fileChooser.isVisible() );
        enterPath( name );
        clickOn( saveButton );
        expAction = ProfileFileManager.APPROVE;
    }

    assertTrue( dismissErrorDialog() );
    Utils.join( thread );
    assertFalse( adHocResult );
    validateState( null, false, expAction );
}
  • Line 2: This is the given operation, for example, () -> fileMgr.save( readOnlyFile ).
  • Line 3: This is the given file.
  • Line 4: This parameter must be true if the operation requires interaction with the file chooser.
  • Lines 7-10: Formulate the Runnables to drive the dedicated thread. Our dedicated thread will schedule the operation on the EDT and wait for it to complete.
  • Line 11: Get the name of the given file; if necessary, it will be entered into the chooser dialog (see line 22).
  • Line 12: Anticipate the value of the file manager’s lastAction property after the operation completes. If expectChooser is false, the expected value will be unchanged from the start of the operation. Otherwise, it will be APPROVE. See also line 24.
  • Lines 14,15: Instantiate and start the dedicated thread.
  • Lines 17-25: If operator interaction with the file chooser is expected:
    • Line 19: Give the chooser dialog time to post.
    • Line 20: Update references to the chooser’s components.
    • Line 21: Verify that the chooser dialog is visible.
    • Lines 22,23: Enter the name of the given file into the chooser and push the dialog’s Save button.
    • Line 24: Update the expected action to APPROVE.
  • Line 27: Verify that an error dialog was posted.
  • Line 28: Wait for the thread to expire.
  • Line 29: Verify the return value of the operative method.
  • Line 30: Verify the file manager’s expected state following operation completion.

🟦 private void cancelOperation(Supplier<?> supplier, File file)
This method begins an operation on the given file, cancels it via the file chooser, and validates the subsequent state of the file manager. The operation may be for input (open) or output (save); it will be executed via a dedicated thread started in the context of the EDT. It assumes that the operation requires interaction with the file chooser; it waits for the chooser dialog to become visible, enters the name of the given file, and pushes the dialog’s Cancel button. Here’s the annotated code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void cancelOperation(
    Supplier<?> supplier, 
    File file
)
{
    Runnable    edtRunner   = () -> supplier.get();
    Runnable    runner      = 
        () -> GUIUtils.schedEDTAndWait( edtRunner );
    String      name        = file.getName();
    File        expFile     = fileMgr.getCurrFile();
    boolean     expResult   = fileMgr.getLastResult();
    Thread      thread      = new Thread( runner );
    thread.start();
    
    Utils.pause( pauseInterval );
    getFileChooserComponents();
    assertTrue( fileChooser.isVisible() );
    enterPath( name );
    clickOn( cancelButton );

    Utils.join( thread );
    validateState( expFile, expResult, ProfileFileManager.CANCEL );
}
  • Line 2: This is the given operation, for example, () -> fileMgr.save( distinctProfile, adHocFile ) or () -> fileMgr.open( baseFile ).
  • Line 3: This is the given file.
  • Line 6-8: Formulate the Runnables to drive the dedicated thread. Our dedicated thread will schedule the operation on the EDT and wait for it to complete.
  • Line 9: Get the name of the given file.
  • Lines 10,11: Record the values of the file manager’s currFile and lastResult properties. Canceled operations do not change the values of these properties, so these will be the expected values after the operation is completed.
  • Lines 12,13: Instantiate and start the dedicated thread.
  • Line 15: Give the file chooser time to post.
  • Lines 16-19: Update references to the file chooser dialog components; enter the file name and push the dialog’s Cancel button.
  • Lines 21,22: Wait for the dedicated thread to expire and validate the file manager’s state.

🟦 private void setLastAction( int action )
This method sets the value of the file manager’s lastAction property to the given value. It does so by initiating an operation requiring file chooser interaction and approving or canceling it. If the desired action is APPROVE, the file manager’s currFile and lastResult properties will also be updated. The code looks like this:

private void setLastAction( int action )
{
    Supplier<Profile>   supplier    = () -> fileMgr.open();
    if ( action == ProfileFileManager.APPROVE )
        openGoRight( supplier, baseFile, true );
    else
        cancelOperation( (Supplier<?>)supplier, adHocFile );
    assertEquals( action, fileMgr.getLastAction() );
}

🟦 private void setLastResult( boolean expResult )
This method configures the file manager’s lastResult property to the given value. It does this by initiating a file manager operation. To set the value to true it executes an operation that is expected to succeed, otherwise it executes an operation that is expected to fail. The file manager’s currFile property will also be updated. A listing of this method follows.

private void setLastResult( boolean expResult )
{
    int expAction   = fileMgr.getLastAction();
    if ( expResult )
    {
        Supplier<Profile> supplier  = () -> fileMgr.open( baseFile );
        openGoRight( supplier, baseFile, false );
        validateState( baseFile, true, expAction );
    }
    else
    {
        Supplier<Profile>   supplier  = 
            () -> fileMgr.open( noSuchFile );
        openGoWrong( supplier, noSuchFile, false );
        validateState( null, false, expAction );
    }
}

🟦 private void initFileManagerState()
This method initializes the file manager state as follows:

  • adHocFile is deleted
  • The lastAction property is set to CANCEL
  • The currFile property is set to null
  • The lastResult property is set to false

The code looks like this.

private void initFileManagerState()
{
    adHocFile.delete();
    assertFalse( adHocFile.exists() );
    setLastAction( ProfileFileManager.CANCEL );
    setLastResult( false );
    fileMgr.close();
    validateState( null, false, ProfileFileManager.CANCEL );
};

⬛ Before/After Methods
Below, we discuss this class’s setup and tear-down methods.

🟦 public static void afterAll() throws Exception
This method is executed after all test methods have been executed. Its job is to clean up all details related to test files. See also the ProfileFileManagerTestData Class. Here’s the code.

@AfterAll
public static void afterAll() throws Exception
{
    ProfileFileManagerTestData.shutdown();
}

🟦 public void beforeEach() throws Exception
The beforeEach method configures the test environment to a known state. The PropertyManager is restored to the state contained in baseProfile, the references to the file chooser components (chooserName, saveButton, etc.) are set to null, and the file manager state is initialized; see initFileManagerState(). A listing for this method follows.

@BeforeEach
public void beforeEach() throws Exception
{
    baseProfile.apply();
    chooserName = null;
    openButton = null;
    saveButton = null;
    cancelButton = null;
    initFileManagerState();
}

Principal Test Methods

We will divide our discussion of test methods into two categories: tests for I/O operations and miscellaneous tests such as testGetLastResult and testGetCurrFile. We’ll start with the miscellaneous test methods.

⬛ Miscellaneous Test Methods
Below, you will find a discussion of test methods that do not directly apply to I/O operations.

🟦 public void testProfileFileManager()
The constructor’s test method doesn’t do much. We don’t know the lastResult or lastAction properties immediately after construction; we can test the currFile property to ensure it’s null. Other than that, we mostly care that it doesn’t crash. Here’s the test method.

@Test
public void testProfileFileManager()
{
    ProfileFileManager  test    = new ProfileFileManager();
    test.getLastResult();
    assertNull( test.getCurrFile() );
}

🟦 public void testGetCurrFile()
This method tests the validity of the getCurrFile method. When the method is first entered, we verify that it returns null. Then, we open a file and verify that getCurrFile returns the opened file. It looks like this:

@Test
public void testGetCurrFile()
{
    assertNull( fileMgr.getCurrFile() );
    Supplier<Profile>   supplier    = () -> fileMgr.open( baseFile );
    openGoRight( supplier, baseFile, false );
    File    currFile    = fileMgr.getCurrFile();
    assertTrue( compareFiles( baseFile, currFile ) );
}

🟦 public void testGetLastResult()
This method sets the file manager’s lastResult property to true and ensures that getLastResult returns true; then, we repeat the test for false. At the start of the test, we verify that it returns false, which is the default value set in initFileManagerState(). The following is a listing of this test method.

@Test
public void testGetLastResult()
{
    assertFalse( fileMgr.getLastResult() );
    
    setLastResult( true );
    assertTrue( fileMgr.getLastResult() );
    
    setLastResult( false );
    assertFalse( fileMgr.getLastResult() );
}

🟦 public void testClose()
To test the file manager’s close method, we open a file, close it, and verify the getCurrFile method’s return value. It looks like this.

@Test
public void testClose()
{
    Supplier<Profile>   supplier    = () -> fileMgr.open( baseFile );
    openGoRight( supplier, baseFile, false );
    assertNotNull( fileMgr.getCurrFile() );
    
    fileMgr.close();
    assertNull( fileMgr.getCurrFile() );
}

🟦 public JFileChooser getFileChooser()
This method validates the file manager’s getFileChooser method. As you can see, it’s very simple.

@Test
public void testGetFileChooser()
{
    assertNotNull( fileMgr.getFileChooser() );
}

⬛ Test Methods for I/O Operations
The ProfileFileManager has ten public methods that initiate I/O operations. Each needs a go-right test (to verify the method’s behavior when an operation completes successfully) and a go-wrong test (to verify the method’s behavior when it fails). Every method that requires operator interaction via the file chooser requires a third test to confirm that it behaves correctly when the operator presses the file chooser’s Cancel button. Finally, the file manager’s save(Profile) method has a special case; if a file is open (getCurrFile returns non-null), we must verify that it saves the profile to the open file. If no file is open, we verify that the file chooser is posted and the profile is saved to a file of the operator’s choice.

The following is a list of all our test methods. We will not discuss every test method in detail. If a discussion is available, there will be a link to it in the list. Otherwise, please treat the implementation of the method as an exercise. All the code can be found in the GitHub repository.

ProfileFileManager I/O Operation Test Cases
Method Under Test Test Method Remarks
open() testOpenGoRight()

Open a file of the operator’s choice; expect successful completion.

testOpenGoWrong()

Open a file of the operator’s choice; expect I/O failure.

testOpenCancel()

Open a file of the operator’s choice; cancel the operation in the file chooser.

open( Profile profile ) testOpenProfileGoRight()

Read a file of the operator’s choice into a given profile; expect successful completion.

testOpenProfileGoWrong()

Read a file of the operator’s choice into a given profile; expect I/O failure.

testOpenProfileCancel()

Read a file of the operator’s choice into a given profile; cancel the operation in the file chooser.

open( File file ) testOpenFileGoRight()

Open the given file; expect successful completion.

testOpenFileGoWrong()

Open the given file; expect I/O failure.

open( File file, Profile profile ) testOpenFileProfileGoRight()

Read the given file into the given profile; expect successful completion.

testOpenFileProfileGoWrong()

Read the given file into the given profile; expect I/O failure.

newFile() testNewFileGoRight()

Create a file of the operator’s choice; expect successful completion.

testNewFileGoWrong()

Create a file of the operator’s choice; expect I/O failure.

testNewFileCancel()

Begin to create a file of the operator’s choice; cancel the operation in the file chooser.

save( Profile profile ) testSaveProfile_CurrFileGoRight()

Save the given profile to the currently open file; expect successful completion.

testSaveProfile_CurrFileGoWrong()

Save the given profile to the currently open file; expect I/O failure.

testSaveProfile_NoCurrFileGoRight()

Save the given profile when there is no open file; the operator will select a file from the file chooser. Expect successful completion.

testSaveProfile_NoCurrFileGoWrong()

Save the given profile when there is no open file; the operator will select a file from the file chooser. Expect I/O failure.

testSaveProfile_NoCurrFileCancel()

Begin a save operation when there is no open file; cancel the operation in the file chooser.

save( File file ) testSaveFileGoRight()

Save to a given file; expect successful completion.

testSaveFileGoWrong()

Save to a given file; expect I/O failure.

saveAs() testSaveAsGoRight()

Save a new profile to a file of the operator’s choice; expect successful completion.

testSaveAsGoWrong()

Save a new profile to a file of the operator’s choice; expect I/O failure.

testSaveAsCancel()

Begin an operation to save a new profile to a file of the operator’s choice; cancel the operation in the file chooser.

saveAs( Profile profile ) testSaveAsProfileGoRight()

Save a given profile to a file of the operator’s choice; expect successful completion.

testSaveAsProfileGoWrong()

Save a given profile to a file of the operator’s choice; expect I/O failure.

testSaveAsCancel()

Begin an operation to save a profile to a file of the operator’s choice; cancel the operation in the file chooser.

save( Profile profile, File file ) testSaveProfileFileGoRight()

Save the given profile to the given file; expect successful completion.

testSaveProfileFileGoWrong()

Save the given profile to the given file; expect I/O failure.

🟦 public void testOpenGoRight()
🟦 public void testOpenGoWrong()
🟦 public void testOpenCancel()
These methods test the ProfileFileManager’s open() method. The go-right test opens distinctFile, the contents of which, by default, will be stored in adHocProfile. Then, it verifies that adHocProfile is equal to distinctProfile. The go-wrong test attempts to open noSuchFile, which will cause a file not found error. The cancel test starts to open adHocFile but cancels the operation via the file chooser. The code for these methods follows.

@Test
public void testOpenGoRight()
{
    Supplier<Profile>   supplier    = () -> fileMgr.open();
    openGoRight( supplier, distinctFile, true );
    assertEquals( distinctProfile, adHocProfile );
}

@Test
public void testOpenGoWrong()
{
    Supplier<Profile>   supplier    = () -> fileMgr.open();
    openGoWrong( supplier, noSuchFile, true );
}

@Test
public void testOpenCancel()
{
    Supplier<?> supplier    = () -> fileMgr.open();
    cancelOperation( supplier, adHocFile );
    assertFalse( adHocFile.exists() );
}

🟦 testOpenProfileGoRight()
This method exercises the ProfileFileManager’s open(Profile) method. It opens distinctFile, the contents of which will, by default, be stored in adHocProfile. Then, it verifies that adHocProfile is equal to distinctProfile. It looks like this.

@Test
public void testOpenProfileGoRight()
{
    Profile profile = new Profile();
    Supplier<Profile>   supplier    = () -> fileMgr.open( profile );
    openGoRight( supplier, distinctFile, true );
    assertEquals( distinctProfile, profile );
}

🟦 testOpenFileGoRight()
This method exercises the ProfileFileManager’s open(File) method. It reads distinctFile, the contents of which will, by default, be stored in adHocProfile. Then, it verifies that adHocProfile is equal to distinctProfile. Here’s the code.

@Test
public void testOpenFileGoRight()
{
    Supplier<Profile>   supplier    = 
        () -> fileMgr.open( distinctFile );
    openGoRight( supplier, distinctFile, false );
    assertEquals( distinctProfile, adHocProfile );
}

🟦 testSaveFileGoRight()
🟦 testSaveFileGoWrong()
These methods test the ProfileFileManager’s save(File) method. The go-right test saves the default Profile to adHocProfile, then it verifies that adHocFile contains baseProfile. The go-wrong test attempts to save a Profile to readOnlyFile, which will cause an I/O error. The code for these methods follows.

@Test
public void testSaveFileGoRight()
{
    BooleanSupplier supplier    = () -> fileMgr.save( adHocFile );
    saveGoRight( supplier, adHocFile, false );
    validateFile( baseProfile, adHocFile );
}

@Test
public void testSaveFileGoWrong()
{
    BooleanSupplier supplier    = () -> fileMgr.save( readOnlyFile );
    saveGoWrong( supplier, readOnlyFile, false );
}

🟦 testSaveProfile_CurrFileGoRight()
🟦 testSaveProfile_NoCurrFileGoRight()
🟦 testSaveProfile_CurrFileGoWrong()
The testSaveProfile_CurrFileGoRight method executes a save(Profile) operation when a file is currently open. Since the default is that no file is open at the start of a test, it first opens adHocFile. It expects that the file chooser dialog will not be posted, and the Profile will be saved to the currently open file.

The testSaveProfile_NoCurrFileGoRight method is executed when no file is open, which is the default for the start of a test. It waits for the file chooser to post and selects adHocFile as the destination.

To test the go-wrong logic for an open file, testSaveProfile_CurrFileGoWrong() first opens readOnlyFile and then attempts a save operation.

Here’s the code for these methods.

@Test
public void testSaveProfile_CurrFileGoRight()
{
    // make sure there's a file open; write distinct data to it
    BooleanSupplier  supplier    = 
        () -> fileMgr.save( distinctProfile, adHocFile );
    saveGoRight( supplier, adHocFile, false );
    // sanity check
    validateFile( distinctProfile, adHocFile );

    // save base data to currently open file (adHocFile)
    supplier = () -> fileMgr.save( baseProfile );
    saveGoRight( supplier, adHocFile, false );
    // sanity check
    validateFile( baseProfile, adHocFile );
}
@Test
public void testSaveProfile_NoCurrFileGoRight()
{
    // Preconditions: adHocFile doesn't exist, getCurrFile()a
    // returns null.
    assertFalse( adHocFile.exists() );
    assertNull( fileMgr.getCurrFile() );
    
    BooleanSupplier supplier = () -> fileMgr.save( distinctProfile );
    saveGoRight( supplier, adHocFile, true );
    // sanity check
    validateFile( distinctProfile, adHocFile );
}
@Test
public void testSaveProfile_CurrFileGoWrong()
{
    // read the no-write file
    Supplier<Profile>   readSupplier    = 
        () -> fileMgr.open( readOnlyFile );
    openGoRight( readSupplier, readOnlyFile, false );

    // try to save to currently open file (no-write file)
    BooleanSupplier supplier = () -> fileMgr.save( baseProfile );
    saveGoWrong( supplier, readOnlyFile, false );
}

🟦 public void testSaveProfileFileGoRight()
This method tests the save(Profile profile, File file) method. It saves distinctProfile to adHocFile, then verifies the content of adHocFile. The code looks like this:

@Test
public void testSaveProfileFileGoRight()
{
    BooleanSupplier  supplier    = 
        () -> fileMgr.save( distinctProfile, adHocFile );
    saveGoRight( supplier, adHocFile, false );
    validateFile( distinctProfile, adHocFile );
}

Summary

We implemented the JUnit test class for the ProfileFileManager class on this page. Although we had many test cases to consider, we simplified the test method implementation by encapsulating much of the common code in helper methods and the ProfileFileManagerTestData class. On the next page, we will focus on the ProfileEditorFeedback window, a critical element in developing a Profile editor. Because the feedback window will have a lot of functionality already developed for the CartesianPlane class, we will move the existing code from CartesianPlane to a dedicated class, GraphManager.

Next: GraphManager, ProfileEditorFeedback Window