Cartesian Plane Lesson 12: Testing

Following is a description of the unit tests that were written/modified for the code introduced in Cartesian Plane, Lesson 12.

Cartesian Plane Lesson 12: GUI Beginnings

GitHub repository: Cartesian Plane Part 12

Lesson 12, Review

In this lesson the following input module classes were introduced or modified, and require testing.

  • FileManager
    A new class which saves and restores equations to files.
  • ItemSelectionDialog
    A new class which allows the operator to make a selection from a list displayed in a dialog.
  • Polar
    A new class that encapsulates the radius and angle components of a polar coordinate pair, and can perform conversions to and from Cartesian coordinate pairs.
  • Exp4jEquation
    An existing class with new features related to maintaining a name property, and producing either theta plots or radius plots from an equation written in polar coordinates.
  • InputParser
    An existing class that has been updated to respond to new commands added to the Command enumeration.

With the exception of FileManager and ItemSelectionDialog, testing of new classes and features turn out to be a fairly simple exercise. The two exceptions are more difficult because this will be our first attempt at tests requiring operator interaction with GUI components. Our solution to this problem, for now, will utilize Java’s Robot class, which is capable of simulating operator keystrokes, mouse movement and mouse button presses. We’ll write the tests for these classes last. See Introduction to the Robot Class, below.

The Polar Class

The main strategy behind testing this class will be driven by a set of class variables reflecting Cartesian coordinate pairs and their corresponding polar coordinate pairs. For convenience, we will also declare class variables for Polar and Point2D objects encapsulating the polar and Cartesian coordinates. Here are our class variable declarations.

Note: recall the formula for converting from polar coordinates (r,t) to Cartesian coordinates (x,y) is:

x = r * cosine(t)
y = r * sine(t)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private static final double testRadius  = 2;
private static final double testTheta   = Math.PI / 3;

private static final double testXco     = 
    testRadius * Math.cos( testTheta );
private static final double testYco     = 
    testRadius * Math.sin( testTheta );

private static final Polar      testPolar   = 
    Polar.of( testRadius, testTheta );
private static final Point2D    testPoint   = 
    new Point2D.Double( testXco, testYco );

getRadius
getTheta
I am going to assume that testing this class’s getters is trivial, and requires no explanation. Here’s one of them; they can both be found in the GitHub repository.

@Test
void testGetRadius()
{
    assertEquals( testRadius, testPolar.getRadius() );
}

toPoint()
of( Point2D point )
The only thing special about testing these methods is that they use the JUnit assertion equals overload that requires three arguments, where the third argument is the epsilon value for determining approximate equality between the first two arguments. Here’s the tester for of(Point2D):

@Test
void testOfPoint2D()
{
    Polar   actPolar    = Polar.of( testPoint );
    assertEquals( testPolar.getRadius(), actPolar.getRadius(), .0001 );
    assertEquals( testPolar.getTheta(), actPolar.getTheta(), .0001 );
}

ofXY( double xco, double yco )
radiusOfXY( double xco, double yco )
thetaOfXY( double xco, double yco )
Tests for these methods derive easily from our class variable declarations; here’s the test for ofXY:

void testOfXY()
{
    Polar   actPolar    = Polar.of( testRadius, testTheta );
    assertEquals( testRadius, actPolar.getRadius() );
    assertEquals( testTheta, actPolar.getTheta() );
}

The Exp4jEquation Class

The changes to this class that we introduced in this lesson are:

  • Setter and getter for name
  • Setter and getter for radius name
  • Setter and getter for theta name
  • Setter and getter for r-expression
  • Setter and getter for t-expression
  • rPlot
  • tPlot

Testing the setters and getters for the first three items is trivial, and I see no need to discuss them. They’re in the GitHub repository if you want to look at them. Testing the setters and getters for the r- and t-expressions is a bit more involved; we have to make sure the string passed to the setter is returned by the getter, and we also have to verify that the string passed to the setter is correctly translated into an Exp4jExpression, and that an invalid string passed to the setter will fail correctly. Let’s look at the tests for r-expression:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testSetRExpression()
{
    String  rExpr   = "0 + 1";
    Result  result  = equation.setRExpression( rExpr );
    assertTrue( result.isSuccess() );
    assertEquals( rExpr, equation.getRExpression() );
    
    equation.setRange( Math.PI, Math.PI, 1 );
    equation.rPlot()
        .forEach( p -> assertEquals( -1, p.getX(), .0001 ) );
}

@Test
public void testSetRExpressionGoWrong()
{
    String  oldRExpr    = equation.getRExpression();
    String  rExpr       = "undeclaredVarName * x";
    Result  result      = equation.setRExpression( rExpr );
    assertFalse( result.isSuccess() );
    assertEquals( oldRExpr, equation.getRExpression() );
}

Test for Failure Notes:

  • Lines 17-21: Tests the case where an invalid expression is passed to setRExpression.
  • Line 17: Gets the current r-expression.
  • Line 18: Defines an expression containing an undeclared variable name.
  • Lines 19-20: Verifies that setRExpression correctly identifies the expression as invalid.
  • Line 21: Verifies that the value of the r-expression was not changed.

Test for Success Notes:

  • Line 4: Sets the equation for calculating the radius to r = 1.
  • Lines 5-6: Verifies that a valid r-expression is successfully set in the equation object.
  • Line 7: Verifies that the getter for the r-expression returns the correct value.
  • Line 9: Sets the iteration range for a plot. This range will produce a stream of one element where theta equals π. Given that radius is always one, this gives the polar coordinate pair (1,π). And of course we know that π on the unit circle has Cartesian coordinates (-1,0).
  • Line 11: Verifies that the x-coordinate of the Cartesian coordinate pair is -1.

The tests for t-expression are nearly identical. You can find them in the GitHub repository.

The test for rPlot is closely related. We set up a range of 0 to (3π)/2 with a step of π/2, and an r-expression of r=1. This yields four points on the unit circle, with Cartesian coordinates (1,0), (0,1), (-1,0) and (0,-1). We use rPlot to generate the coordinates, and validate against expected values. Here’s the 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
@Test
public void testRPlot()
{
    // 4 points where unit circle intersects x/y axes
    List<Point2D>   expPoints   = List.of( 
        new Point2D.Double( 1, 0 ), 
        new Point2D.Double( 0, 1 ), 
        new Point2D.Double( -1, 0 ), 
        new Point2D.Double( 0, -1 )
    );
    
    equation.setRExpression( "1" );
    double  start   = 0;
    double  end     = 3 * Math.PI / 2;
    double  step    = Math.PI / 2;
    
    equation.setRange( start, end, step );
    List<Point2D>   actPoints   =
        equation.rPlot()
        .collect( Collectors.toList() );
    
    // Test equality after allowing for rounding errors
    double  epsilon = .000001;
    assertEquals( expPoints.size(), actPoints.size() );
    IntStream.range( 0, 4 ).forEach( i -> {
        Point2D ePoint  = expPoints.get( i );
        Point2D aPoint  = actPoints.get( i );
        assertEquals( ePoint.getX(), aPoint.getX(), epsilon, "" + i );
        assertEquals( ePoint.getY(), aPoint.getY(), epsilon, "" + i );
    });
}

Notes:

  • Lines 5 -10: Establishes a list of expected values.
  • Line 12: Sets the expression r=1.
  • Lines 13-17: Sets the iteration range as described above.
  • Line 19: Generates the four Cartesian coordinate pairs.
  • Line 20: Saves the generated coordinates to a list.
  • Lines 23-30: Compares the generated values against the expected values.

The test for tPlot is similar, and can be found in the GitHub repository.

InputParser

The only additional testing for this class is focused on the new commands added to the Command enum: RPLOT, TPLOT, REQUALS, TEQUALS, RADIUS and THETA. Tests for the first two are easy; the commands are added to the list of parameters being tested in the testParseInputNOOP method:

@ParameterizedTest
@ValueSource( strings= 
    {"EXIT","NONE","YPLOT","XYPLOT","RPLOT","TPLOT","OPEN","SAVE" } 
)
public void testParseInputNOOP( String strCommand )
{
    Command command = Command.toCommand( strCommand );
    Result  result  = parser.parseInput( command, "" );
    assertTrue( result.isSuccess() );
}

The tests for last four are likewise simple, thanks to use of the testSetString helper method:

@Test
public void testParseInputREQUALS()
{
    Equation        equation    = parser.getEquation();
    String          newVal      = "a + a + a";
    testSetString( Command.REQUALS, newVal, equation::getRExpression );
}

@Test
public void testParseInputTEQUALS()
{
    Equation        equation    = parser.getEquation();
    String          newVal      = "a + a + a";
    testSetString( Command.TEQUALS, newVal, equation::getTExpression );
}

@Test
public void testParseInputRADIUS()
{
    Equation        equation    = parser.getEquation();
    String          newVal      = "newRadiusName";
    testSetString( Command.RADIUS, newVal, equation::getRadiusName );
}

@Test
public void testParseInputTHETA()
{
    Equation        equation    = parser.getEquation();
    String          newVal      = "newThetaName";
    testSetString( Command.THETA, newVal, equation::getThetaName );
}

That leaves us with the FileManager and ItemSelectionDialog classes to test. These are the classes that need to test operator interaction with a GUI. So first we’ll look at the Robot class, a Java utility that simulates operator input.

Introduction to the Robot Class

Robot is a Java utility class capable of simulating keystrokes and mouse actions. This makes it convenient for automating tests that require operator interaction. Robot is very low level, and very finicky, and we’ll eventually learn a better way for automating tests like these, but it is definitely worth becoming familiar with Robot. In this lesson we will use its ability to simulate keystrokes to test the ItemSelectionDialog class, and those portions of the FileManager class that rely on interaction with JFileChooser. We won’t be talking about its mouse-related capabilities.

■ Initializing a Robot

To work with the Robot class you need an instance of that class. The constructor can throw an AWTException, which is a checked exception so you have to be prepared to catch it. After instantiation you probably want to set the Robot’s autoDelay property. This will cause the Robot to simulate the time required to execute an action. I typically use 10 milliseconds:

try
{
    Robot   robot   = new Robot();
    robot.setAutoDelay( 10 );
}
catch ( AWTException exc )
{
    System.err.println( exc.getMessage() );
    System.exit( 1 );
}

■ Typing a Character

When an operator types a character, two events are generated: a key press followed by a key release. If I want a Robot to simulate typing a character I have to issue two corresponding instructions, using:
    void keyPress( int keyCode ), and
    void keyRelease( int keyCode ).
I probably want to simulate a small delay between the two events, but if I’ve initialized the Robot as I did above, the Robot will take care of that automatically.

Note that the value passed to these methods is type int, not char. And as the name of the parameter suggests, we are passing an integer code that corresponds to a key, not a Unicode value. This makes sense. You might want to simulate typing the shift key, an arrow key or one of the functions keys; these keys, and many others, are not associated with Unicode values.

The key codes for each of the keys on your keyboard are declared in the KeyEvent class as a constant variable with a name beginning with VK_. Some of these variables are listed in the following table.

Name         Hex Char
VK_COMMA      2c  ,
VK_MINUS      2d  -
VK_PERIOD     2e  .
VK_SLASH      2f  /
VK_0          30  0
VK_1          31  1
VK_2          32  2
VK_3          33  3
VK_4          34  4
VK_5          35  5
VK_6          36  6
VK_7          37  7
VK_8          38  8
VK_9          39  9
Name         Hex Char
VK_SEMICOLON  3b  ;
VK_EQUALS     3d  =
VK_A          41  A
VK_B          42  B
VK_C          43  C
VK_D          44  D
VK_E          45  E
VK_F          46  F
VK_G          47  G
VK_H          48  H
VK_I          49  I
VK_J          4a  J
VK_K          4b  K
VK_L          4c  L
Name         Hex Char
VK_M          4d  M
VK_N          4e  N
VK_O          4f  O
VK_P          50  P
VK_Q          51  Q
VK_R          52  R
VK_S          53  S
VK_T          54  T
VK_U          55  U
VK_V          56  V
VK_W          57  W
VK_X          58  X
VK_Y          59  Y
VK_Z          5a  Z

You’ll note that there is some correspondence between key codes and Unicode values. For example, the A key has a key code that maps to the Unicode value for ‘A’, the one key has a key code that maps to the Unicode value for ‘1’ , and the semicolon key has a key code that maps to the Unicode value for ‘;’. Note, however, there is no key code that maps to ‘a’ or ‘:’. If I use a Robot to to execute the sequence keyPress( VK_A ), keyRelease( VK_A) that will produce the character ‘a’, just as if I physically typed the A key myself. If I want an upper case ‘A’ have to hold down the shift key while typing the A key. The sequence using a Robot would look like this:

    robot.keyPress( KeyEvent.VK_SHIFT );
    robot.keyPress( KeyEvent.VK_A );
    robot.keyRelease( KeyEvent.VK_A );
    robot.keyRelease( KeyEvent.VK_SHIFT );

On my keyboard (your keyboard may be different), the colon and semicolon are located on the same key; to get the colon you have to hold down the shift key. If you want to use Robot to type a colon you would not use VK_COLON; instead you would use VK_SHIFT, VK_SEMICOLON:

    robot.keyPress( KeyEvent.VK_SHIFT );
    robot.keyPress( KeyEvent.VK_SEMICOLON );
    robot.keyRelease( KeyEvent.VK_SEMICOLON );
    robot.keyRelease( KeyEvent.VK_SHIFT );

Here are a few more Robot examples that work on my keyboard; your results may differ!

Events             Result
keyPress(VK_1)        1
keyRelease(VK_1) 
Events             Result
keyPress(VK_SHIFT)    !
keyPress(VK_1)
keyRelease(VK_1) 
keyRelease(VK_SHIFT)
Events             Result
keyPress(VK_SLASH)   /
keyRelease(VK_SLASH) 
Events             Result
keyPress(VK_SHIFT)    ?
keyPress(VK_SLASH)
keyRelease(VK_SLASH) 
keyRelease(VK_SHIFT)

Suppose I want to translate a string into a series key presses/releases. I’m pretty sure I can come up with a good strategy if I stick to spaces and alphanumeric characters. But if I want to do something like type “C:\tmp” into the JFileChooser dialog I have a much tougher job making it work for my keyboard and yours. This code fragment from the project sandbox program RobotDemo1 shows how type the space and alphanumeric characters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private static void type( String str, Robot robot )
{
    for ( char ccc : str.toCharArray() )
    {
        if ( canTranslate( ccc ) )
        {
            boolean isUpper = Character.isUpperCase( ccc );
            char    upper   = Character.toUpperCase( ccc );
            if ( isUpper )
                robot.keyPress( KeyEvent.VK_SHIFT );
            robot.keyPress( upper );
            robot.keyRelease( upper );
            if ( isUpper )
                robot.keyRelease( KeyEvent.VK_SHIFT );
        }
    }
    robot.keyPress( KeyEvent.VK_ENTER );
    robot.keyRelease( KeyEvent.VK_ENTER );
}

Notes:

  • Line 3: Executes a loop for every character in str.
  • Line 5: Ignores every character that is not alphanumeric or a space.
  • Line 7: Sets a flag indicating whether this is an uppercase alphabetic character.
  • Line 8: Generates the key code for the subject character. Remember that:
    • The key code for ‘a’ and ‘A’ is the same, VK_A.
    • For alphabetic characters, the key code is the same as the Unicode value for the uppercase character.
    • Character.toUpperCase ignores non-alphabetic characters like numbers and spaces.
  • Lines 9-10: Presses the shift key, if we’re trying to type an uppercase letter.
  • Line 11: Presses the key for the target character.
  • Line 12: Releases the key for the target character.
  • Line 13-14: If necessary, releases the shift key.
  • Lines 17-18: Adds a line separator to the output.

So far that’s a lot of discussion about key-presses and key-releases, but we still have not come up with a good solution for typing “C:\tmp” into the JFileChooser dialog. I found this suggestion on StackOverflow.com: to simulate typing a string into a textbox:

  1. Copy the string to the system clipboard.
  2. Execute a paste operation by using Robot to type either command-V (Mac OS) or control-V (everyplace else).

RobotPasteDemo in the project sandbox shows us how to do this. But before we get to that, let’s look at another problem that needs to be solved.

■ Displaying and Interacting with a Modal Dialog

The problem at hand stems from this scenario:

Suppose you want to start a JFileChooser dialog, then simulate operator interaction. You call the file chooser’s showOpenDialog method and… then what? JFileChooser is a modal dialog, and its show method won’t return until the dialog is dismissed, so how are you going to execute the code that “simulates operator interaction”?

The solution is to start the file chooser dialog in its own thread. Then it’s the file chooser thread that gets suspended until the dialog is dismissed, and the main thread, the one that’s executing your test code, can do the desired simulation. Here’s how you start the file chooser in its own thread:

  1. Instantiate a Thread. The Thread constructor we’ll use requires as an argument a Runnable object. Runnable is a functional interface; its sole abstract method is void run(), which returns nothing and requires no arguments. The functional interface we use will be implemented via the runFileChooser( boolean open ) helper method:
        Thread chooserThread = new Thread(() -> runFileChooser( open ));
  2. Once you have a thread it needs to be started:
        chooserThread.start();
  3. After calling a thread’s start method, we don’t know exactly when the thread will actually begin execution. First, it has to be scheduled to run. Once it’s scheduled the thread could begin executing immediately; or the system might return to your code, and begin executing the statement after the chooserThread.start() instruction; or there might be other threads already scheduled to be run, and your chooserThread will have to wait for them to complete before getting its turn. To make things a bit more unpredictable, this particular thread has to instantiate a JFileChooser, and manufacture the GUI that will display it, a potentially time consuming operation. So after starting the chooserThread our main code is going to wait, or sleep for a bit before continuing (during this time we say that our main thread is suspended). Erring on the side of caution, we’ll sleep for a full second (1000 milliseconds):
        Thread.sleep( 1000 );

After waking, our main thread can simulate the operator interaction. The last action it takes at this time will be to type either the enter key, causing the file chooser’s OK button to be activated, or the escape key, causing the operation to be cancelled. Having done so we have to wait for the dialog to be dismissed; we can do that by joining the chooserThread:
    type( tempFile1.getAbsolutePath(), lastKey );
    chooserThread.join();

Note: the sleep and join methods in the Thread class potentially throw InterruptedException, so you have to be prepared to catch this exception, or declare that you throw it.

The type helper method uses the Robot to perform the simulated operator interaction. Before we look at that, let’s review the code that manipulates the chooserThread. The main code is encapsulated in the runDemo(boolean open, int lastKey) method. The open argument controls how we display the file chooser dialog, using either its showOpenDialog (true) method or its showSaveDialog (false) method. The lastKey value will be either VK_ENTER (approve action) or VK_ESCAPE (cancel action). After joining the chooserThread it calls the showResult method, which tells us whether the file chooser dialog was closed with the OK button, and which file was selected, if any. The runFileChooser method starts the file chooser, then saves the result in class variables, where they can examined by the code in showResult method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    private static void runDemo( boolean open, int lastKey )
        throws InterruptedException
    {
        Thread  chooserThread   = new Thread( () -> runFileChooser( open ) );
        chooserThread.start();
        Thread.sleep( 1000 );
        
        type( tempFile1.getAbsolutePath(), lastKey );
        chooserThread.join();
        showResult();
    }
    
    private static void runFileChooser( boolean open )
    {
        if ( open )
            fileChooserStatus = chooser.showOpenDialog( null );
        else
            fileChooserStatus = chooser.showSaveDialog( null );
        fileChooserFile  = chooser.getSelectedFile();
    }

■ Simulating Operator Input with the Clipboard

This job is performed using the system clipboard in conjunction with a Robot. The first task is to get the system clipboard from the default toolkit:
    Clipboard clipboard =
        Toolkit.getDefaultToolkit().getSystemClipboard();

Note: the Toolkit is the place to go when you’re looking for system resources such as the clipboard and print queue, and information about resources such as the screen size. It’s worth having a look to see what it has to offer. See the Toolkit Javadoc, in the Oracle Java documentation.

Note that all sorts of resources can be stored in the clipboard, including images, rich text and files. To store something in the clipboard you need to wrap it in a Transferable appropriate for its type; we are storing a String, so we need a StringSelection:
    String str = "C:\tmp\tempfile.txt";
    StringSelection selection = new StringSelection( str );

Now that we have a StringSelection we can store it in the clipboard using the setContent method. This method requires two arguments, the Transferable and the clipboard owner; we don’t need an owner, so we will pass null for the second argument:
    clipboard.setContents( selection, null );

Once we have stored the desired text in the clipboard we can use Robot to execute the paste shortcut:
    // pasteModifier = VK_META (Mac) or VK_CONTROL (all others)
    robot.keyPress( pasteModifier );
    robot.keyPress( KeyEvent.VK_V );
    robot.keyRelease( KeyEvent.VK_V );
    robot.keyRelease( pasteModifier );

Here’s the type method from RobotPasteDemo in the project sandbox. Line 18, Thread.sleep( 2000 ), is for the benefit of the operator; it gives you a chance to see what’s been pasted into the dialog’s text box before the terminating action is executed, and the dialog disappears.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private static void type( String str, int lastKey )
    throws InterruptedException
{
    String      osName          = 
        System.getProperty( "os.name" ).toLowerCase();
    int         pasteModifier   =
        osName.equals( "mac" ) ? KeyEvent.VK_META : KeyEvent.VK_CONTROL;

    Clipboard   clipboard       = 
        Toolkit.getDefaultToolkit().getSystemClipboard();
    StringSelection selection   = new StringSelection( str );
    clipboard.setContents( selection, null );
    
    robot.keyPress( pasteModifier );
    robot.keyPress( KeyEvent.VK_V );
    robot.keyRelease( KeyEvent.VK_V );
    robot.keyRelease( pasteModifier );
    Thread.sleep( 2000 );
    robot.keyPress( lastKey );
    robot.keyRelease( lastKey );
}

◼ Other Elements of RobotPasteDemo

There are a couple of other bits to the demo we’ve been looking at. They deal with pointing files to the system’s temporary directory, and making sure that files created during the demo (if any) eventually get cleaned up. We’ll be putting the same logic into the FileManager JUnit test, so we’ll talk about the code at that time. Meanwhile, it can be found, as usual, in the GitHub repository.

The RobotAssistant Class

I have created a new class, RobotAssistant, to encapsulate the typing logic discussed above. I put it in the test_utils package on the src/test/java source code branch. For support of testing, I have added some methods to type the four arrow keys. If you’ve read the previous section of the notes there won’t be any surprises in it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class RobotAssistant
{
    private static final String     osName          = 
        System.getProperty( "os.name" ).toLowerCase();
    private static final int        pasteModifier   =
        osName.equals( "mac" ) ? KeyEvent.VK_META : KeyEvent.VK_CONTROL;

    private static final int        pasteKey        = KeyEvent.VK_V;
    private static final Clipboard  clipboard       = 
        Toolkit.getDefaultToolkit().getSystemClipboard();
    
    private final Robot robot;
    
    public RobotAssistant() throws AWTException
    {
        robot = new Robot();
        robot.setAutoDelay( 10 );
    }
    
    public void type( String chars, int lastKey )
    {
        StringSelection selection    = new StringSelection( chars );
        clipboard.setContents( selection, null );
        
        robot.keyPress( pasteModifier );
        robot.keyPress( pasteKey );
        robot.keyRelease( pasteKey );
        robot.keyRelease( pasteModifier );
        robot.keyPress( lastKey );
        robot.keyRelease( lastKey );
    }
    
    public void upArrow()
    {
        robot.keyPress( KeyEvent.VK_UP );
        robot.keyRelease( KeyEvent.VK_UP );
    }
    
    public void downArrow()
    {
        robot.keyPress( KeyEvent.VK_DOWN );
        robot.keyRelease( KeyEvent.VK_DOWN );
    }
    
    public void leftArrow()
    {
        robot.keyPress( KeyEvent.VK_LEFT );
        robot.keyRelease( KeyEvent.VK_LEFT );
    }
    
    public void rightArrow()
    {
        robot.keyPress( KeyEvent.VK_RIGHT );
        robot.keyRelease( KeyEvent.VK_RIGHT );
    }
}

The FileManager Class

Let’s review the architecture of this class. The class consists of a hierarchy of methods used to either open or save an equation file. At the top of the hierarchy are two methods:
    Equation open( BufferedReader bufReader )
    void save( PrintWriter pWriter, Equation equation )

which perform most of the work. All the methods lower in the hierarchy provide different ways to reach the top:

Create a BufferedReader or a PrintWriter from the given file, and invoke open( BufferedReader ), or save( Equation, PrintWriter )
Equation open( File file )
void save( File file, Equation equation )
 
Create a File from the given string, and invoke open( File ), or save( Equation, File)
Equation open( String path )
void save( String path, Equation equation )
 
Prompt the operator for a file, and invoke open( File ), or save( Equation, File)
Equation open()
void save( Equation equation )

Given the hierarchical nature of the code under test, it seems like we should be a to create one set of test data, including a file in the system temporary directory, then test the methods in pairs. Once the initial data is configured, the strategy would look something like this:

  1. Delete the temporary file.
  2. Save the test equation to the temporary file.
  3. Read the test equation back in.
  4. Verify that the opened equation is equivalent to the test equation.

We’ll start by encapsulating the test data in class variables; here are their declarations followed by a description.

 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
class FileManagerTest
{
    private static final Map<String,Double> testVarMap;
    
    private static final String testFileName    = "FileManagerTest.tmp";
    private static final String testFilePath;
    private static final File   testFile;
    
    private static final String testName    = "test noma";
    
    private static final double testStart   = 10.1;
    private static final double testEnd     = testStart * 5;
    private static final double testStep    = 1.1;
    
    private static final String testXEq     = "100";
    private static final String testYEq     = "200";
    private static final String testTEq     = "300";
    private static final String testREq     = "400";
    
    private static final String testParam   = "pTest";
    private static final String testRadius  = "rTest";
    private static final String testTheta   = "yTest";
    
    private Equation    testEquation;
    private Equation    openEquation;
    // ...

Notes:

  • Line 3: This will be the set of variables that we expect to be saved with the test equation, and then read back in with an open command. We’ll make it unmodifiable to guarantee that it won’t be corrupted between tests. It is initialized in a static initialization block which we’ll discuss below.
  • Lines 5-7: A description of the test file which we’ll establish in the system temporary directory. The testFilePath and testFile fields are initialized in the static initialization block, below. We’ll have a @BeforeEach method which will delete the file before every test. We’ll also have an @AfterAll method that deletes it right before the unit test terminates.
  • Lines 9-22: The data used to configure the test equation:
    • The name of the equation;
    • The iteration range;
    • The x-, y-, r- and t-expressions;
    • The names of the parameter, radius and theta variables
  • Line 24: The equation that will be written to the temporary file with each test of a save method. In case it becomes corrupted during a test, it is reinitialized before the next test in the @BeforeEach method.
  • Line 25: When an equation is read during an open test requiring interaction with the file chooser, this is where it will be stored. This variable is initialized to null in the @BeforeEach method.

Here are the static initialization block, and @BeforeEch 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
42
43
44
45
46
47
48
49
static
{
    Map<String,Double>  temp    = new HashMap<>();
    
    // Add the default declarations to the var map
    Equation    equation    = new Exp4jEquation();
    temp.putAll( equation.getVars() );
    
    // Add a few non-default variables; use a double value
    // that can be perfectly represented
    IntStream.range( 'a', 'm' )
        .forEach( c -> temp.put( "" + (char)c, c + 3.1 ) );
    testVarMap = Collections.unmodifiableMap( temp );
    
    String  tempDir = System.getProperty( "java.io.tmpdir" );
    testFilePath = tempDir + File.separator + testFileName;
    testFile = new File( testFilePath );
}

@BeforeEach
public void beforeEach()
{
    testEquation = new Exp4jEquation();
    
    testVarMap.entrySet()
        .forEach( e -> testEquation.setVar( e.getKey(), e.getValue() ));
    
    // set everything else that makes sense
    testEquation.setName( testName );
    testEquation.setRange( testStart, testEnd, testStep );
    testEquation.setXExpression( testXEq );
    testEquation.setYExpression( testYEq );
    testEquation.setRExpression( testREq );
    testEquation.setTExpression( testTEq );
    testEquation.setParam( testParam );
    testEquation.setRadiusName( testRadius );
    testEquation.setThetaName( testTheta );
    
    // This variable starts each test equal to null.
    // In some tests, it will be replaced by a valid equation
    // if the test is successful.
    openEquation = null;
    
    testFile.setWritable( true );
    
    // Destroy pre-existing data
    if ( testFile.exists() )
        testFile.delete();
}

Notes:

  • Line 3: Creates a temporary variable map. We have to initialize the variables in a temporary location before copying it to its final, unmodifiable location.
  • Line 6: Creates an equation so that we can copy all the default variables out of it.
  • Line 7: Copies all the default variables into the temporary variable map.
  • Line 11: Iterates over the range of variable names a through m.
  • Line 12: Adds each variable to the variable map. The values chosen for the variables are values that can be perfectly represented as type double, relieving us of the need for epsilon tests.
  • Line 13: Copies the temporary map to its final location, and makes it unmodifiable.
  • Line 15: Uses the value of the system property java.io.tmpdir to locate the system’s temporary directory.
  • Line 16: Appends the temporary file name to the name of the temporary directory, making an absolute path to the test file. File.separator will be backslash (\) for MS Windows, and slash (/) for Unix and Mac.
  • Line 17: Encapsulates the test file in a File object.
  • Lines 23-26: Creates an Equation object, and adds to it all the variable declarations from the variable map.
  • Lines 29-37: Initializes the remaining properties of the test equation.
  • Line 44: Sets the variable used by the open() tester to null.
  • Line 46: Makes sure that the test file is writable. This is necessary because some tests may make the test file read-only in order to simulate I/O failures.
  • Lines 47-48: Makes sure that the test file is deleted before each new test.

The @AfterAll method is quite simple. It just cleans up our temporary files:

@AfterAll
public static void afterAll()
{
    File    tempFile    = new File( testFilePath );
    if ( tempFile.exists() )
        tempFile.delete();
}

We have one helper method to verify that the fields of the equation read by an open tester matches those of the test equation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private static void assertEqualsDefault( Equation actVal )
{
    assertNotNull( actVal );
    assertEquals( testVarMap, actVal.getVars() );
    assertEquals( testName, actVal.getName() );
    assertEquals( testStart, actVal.getRangeStart() );
    assertEquals( testEnd, actVal.getRangeEnd() );
    assertEquals( testStep, actVal.getRangeStep() );
    assertEquals( testXEq, actVal.getXExpression() );
    assertEquals( testYEq, actVal.getYExpression() );
    assertEquals( testREq, actVal.getRExpression() );
    assertEquals( testTEq, actVal.getTExpression() );
    assertEquals( testParam, actVal.getParam() );
    assertEquals( testRadius, actVal.getRadiusName() );
    assertEquals( testTheta, actVal.getThetaName() );
}

Note: there are a couple of more helper methods that we’ll discuss when we get into the tests involving the JFileChooser dialog.

So, with all that advance preparation, the unit tests for four of the public methods, the open and save methods that take file names and File objects, are easy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
public void testSaveOpenStringEquation()
{
    FileManager.save( testFilePath, testEquation );
    Equation    equation    = FileManager.open( testFilePath );
    assertEqualsDefault( equation );
}

@Test
void testSaveOpenFileEquation()
{
    FileManager.save( testFile, testEquation );
    Equation    equation    = FileManager.open( testFile );
    assertEqualsDefault( equation );
}

Testing the open and save methods that take I/O streams are only slightly more complicated; we have to open and close the I/O streams, and be prepared to catch I/O exceptions:

 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
@Test
public void testSaveOpenStreamEquation()
{
    try (
        PrintWriter printer = new PrintWriter( testFilePath );
    )
    {
        FileManager.save( printer, testEquation );
    }
    catch ( IOException exc )
    {
        exc.printStackTrace();
        fail( "I/O error", exc );
    }
    
    try (
        FileReader fReader = new FileReader( testFilePath );
        BufferedReader bReader = new BufferedReader( fReader );
    )
    {
        Equation    equation    = FileManager.open( bReader );
        assertEqualsDefault( equation );
    }
    catch ( IOException exc )
    {
        exc.printStackTrace();
        fail( "I/O error", exc );
    }   
}

There are two auxiliary tests associated with the above six methods for exercising the I/O error logic. The method saveWithIOError:

  1. Creates the test file.
  2. Makes the test file read-only.
  3. Makes a new test equation with properties different from the default (the equation referenced by the class variable testEquation).
  4. Attempts to save the new equation to the test file.
  5. Verifies that the new equation was not written to the test file.

Here’s the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Test
public void saveWithIOError()
{
    FileManager.save( testFilePath, testEquation );
    assertTrue( testFile.exists() );
    
    testFile.setReadOnly();
    String  oldName = testEquation.getName();
    Equation    newEquation = new Exp4jEquation();
    newEquation.setName( oldName + "_test" );
    
    // try to save new equation
    FileManager.save( testFilePath, newEquation );
    // verify save failed
    newEquation = FileManager.open( testFilePath );
    assertNotNull( newEquation );
    assertEquals( oldName, newEquation.getName() );
}

The test method openWithIOError simply tries to read an equation from a non-existent file, and verifies that the open method returns null:

@Test
public void openWithIOError()
{
    Equation    equation    = FileManager.open( "nobodyhome" );
    assertNull( equation );
}

■ Testing with JFileChooser

To test the open and save methods that interact with the file chooser we need three more helper methods. The startDialog( Runnable runner ) method starts a new thread encapsulating the given Runnable, which is assumed to execute a command that will open the JFileChooser dialog. After starting the thread it waits for one-half second to give the file chooser dialog a chance to be displayed, then returns the new Thread object to the caller. The logic encapsulated here is essentially equivalent to the logic demonstrated in the RobotPasteDemo (see above).

private Thread startDialog( Runnable runner )
{
    Thread  thread  = new Thread( runner );
    thread.start();
    Utils.pause( 500 );
    return thread;
}

The execOpenCommand and execSaveCommand methods constitute the Runnables encapsulated in the thread created by startDialog. They call the FileManager.open() and FileManager.save() methods, respectively. The execOpenCommand saves the value returned by the open method to the class variable openEquation, where it can be examined by the test code in the main thread:

private void execOpenCommand()
{
    openEquation = FileManager.open();
}

private void execSaveCommand()
{
    FileManager.save( testEquation );
}

Test method testSaveOpenEquationApprove tests the case where the operator selects a file from the file chooser, then dismisses it by executing the approve action. Test method testSaveOpenEquationCancel tests the case in which the operator dismisses the file chooser with the cancel option. The first method verifies its results by saving an equation and reading it back, like most of the other test methods. The testSaveOpenEquationCancel method:

  • Starts a save operation;
  • Dismisses the file chooser dialog with the cancel action; and
  • Verifies that the test file has not been created. Then:
  • Saves the test file, making sure that it exists;
  • Starts an open operation;
  • Dismisses the file chooser with the cancel action; and
  • Verifies that the test file has not been read.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Test
public void testSaveOpenEquationApprove() 
    throws AWTException, InterruptedException
{
    Thread          thread  = null;
    RobotAssistant  robot   = new RobotAssistant();
    
    thread  = startDialog( () -> execSaveCommand() );
    robot.type( testFilePath, KeyEvent.VK_ENTER );
    thread.join();
    assertTrue( testFile.exists() );
    
    thread  = startDialog( () -> execOpenCommand() );
    robot.type( testFilePath, KeyEvent.VK_ENTER );
    thread.join();
    assertEqualsDefault( openEquation );
}

@Test
public void testSaveOpenEquationCancel() 
    throws AWTException, InterruptedException
{
    Thread          thread  = null;
    RobotAssistant  robot   = new RobotAssistant();
    
    thread  = startDialog( () -> execSaveCommand() );
    robot.type( testFilePath, KeyEvent.VK_ESCAPE );
    thread.join();
    assertFalse( testFile.exists() );
    
    // put equation out there...
    // start to open the equation...
    // cancel open dialog...
    // verify equation is not read.
    FileManager.save( testFilePath, testEquation );
    thread  = startDialog( () -> execOpenCommand() );
    robot.type( testFilePath, KeyEvent.VK_ESCAPE );
    thread.join();
    assertNull( openEquation );
}

Caveat: When you run these tests, the ones that exercise the I/O logic will cause errors to be displayed in message dialogs. Eventually we will have to learn how to: a) verify that the message was displayed as required; and b) dismiss the dialog. That will be discussed in a later lesson. For now, just assume that you, the tester/operator, have to be prepared for the message dialogs to be displayed, and then dismiss them by clicking the OK button.

The ItemSelectionDialog Class

This is another test requiring simulated operator interaction with a dialog. Before we look at the code, I’m going to have you perform the following exercise.

  1. Start the ItemSelectionDialogDemo1 program in the project sandbox. Note that, after the dialog appears, the first item in the selection list is highlighted.
  2. Push the down arrow on your keyboard twice. Note that the third item in the selection list is now highlighted.
  3. Push the enter key. Note that the dialog is dismissed, and the main method that opened the dialog has detected that the third item in the list (the one at index 2) was selected.

This is pretty much what we’re going to do in our JUnit test: open the dialog, then use the RobotAssistant to simulate pushing the down arrow button and then the OK button. We’ll then verify that the correct item in the list was selected. Then we’ll run a second test in which we’ll tell the RobotAssistant to type the escape key, and verify that the return value from the ItemSelectionDialog’s show method informs us that no selection was made. As in testing the paths through FileManager that utilize the JFileChooser dialog, the dialog will have to be displayed in a separate thread.

Here’s the test class in its entirety. If you run it in the code coverage analyzer, you’ll see that we achieve 100% code coverage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ItemSelectionDialogTest
{
    private final String[]  names   =
    { "Sally", "Manny", "Jane", "Moe", "Anapurna", 
      "Jack", "Alice", "Tweedledee", "Elizabeth", "Tweedledum",
    };
    
    private final ItemSelectionDialog   dialog  =
        new ItemSelectionDialog( "JUnit Test", names );
    
    /** This variable set by show() method. */
    private int selection   = 0;
    
    @Test
    void testShowOK() 
        throws AWTException, InterruptedException
    {
        Thread          thread  = startDialog();
        RobotAssistant  robot   = new RobotAssistant();
        robot.downArrow();
        robot.downArrow();
        robot.type( "", KeyEvent.VK_ENTER );
        thread.join();
        
        assertEquals( 2, selection );
    }

    @Test
    void testShowCancel() 
        throws AWTException, InterruptedException
    {
        Thread          thread  = startDialog();
        RobotAssistant  robot   = new RobotAssistant();
        robot.downArrow();
        robot.type( "", KeyEvent.VK_ESCAPE );
        thread.join();
        
        assertTrue( selection < 0 );
    }
    
    private Thread startDialog()
    {
        Thread  thread  = new Thread( () -> show() );
        thread.start();
        Utils.pause( 500 );
        return thread;
    }
    
    private void show()
    {
        selection = dialog.show();
    }
}