|
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.
|