This is part 2 of the testing of the implementation described in the previous lesson.
GitHub repository: Cartesian Plane Part 11
Previous lesson: Cartesian Plane Lesson 11: User Input, Testing (Page 1)
🔳 public class InputParser
This class only has two public constructors and three public methods. The constructors and two of the public methods are trivial. Almost all our testing is going to be concentrated on one method, parseInput( Command command, String argString ), which contains a switch statement with ten distinct cases. Consequently, instead of following the “usual” JUnit testing pattern, with one or two test methods for each public method, we are, in addition, going to have one or more test methods for each case in parseInput.
Important: I’ve addressed this topic before, but it bears mentioning again. This is a great example of code that prompts students students to ask me, “in my unit testing, how am I supposed to get code coverage on all those private methods?” (there are nine of them). The answer is that all of those methods exist in support of the cases in the switch statement in the public method parseInput(Command command, String argString). You should be able to fully exercise each private method by testing the cases in the switch statement. If you’ve tested everything you can think of, and there’s a private method that has not been exercised, you should be asking, “Why does that method even exist?” With a handful of exceptions, if you’re getting some, but not extensive coverage on a private method, maybe some of the code shouldn’t be there? Perhaps it can be revised to eliminate the lines of code that are not being covered.
Also as mentioned earlier: the above discussion applies specifically to unit testing. If you’re debugging a problem you have a lot of additional options:
-
- Step through the code in the debugger.
- Temporarily give your class a main method that can invoke private methods directly.
- Temporarily add diagnostic code such as print statements.
- Bench test it. This is one of my favorite debugging strategies. Copy the method into a throw-away test program, where the code is affected by a minimum of interference from other methods.
The complaint about bench testing I typically get is “but the code I’m trying to debug is so deeply integrated into a method, or is so dependent on other methods, that I can’t isolate it.” My response to that is: maybe that’s your problem. Rewrite your code so that specific tasks are encapsulated in methods that are dedicated to that task, with limited dependencies on other methods. The rule is, “A method should do one thing, and do it well.”
For convenience, I have declared in the test class an InputParser as an instance variable, and I initialize it in a @BeforeEach method:
private InputParser parser;
@BeforeEach
public void beforeEach()
{
parser = new InputParser();
}
The only awkward thing about testing this class is validating those commands that occasionally have to print to stdout. But we already know how to handle that (see the discussion of sandbox application MemoryOutputDemo1). Here is a pair of methods that support this operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | private void testEmptyArg( Command cmd, String expOutput ) { String actOutput = getStdout( parser, cmd, "" ); assertEquals( expOutput, actOutput ); } private String getStdout( Command cmd, String arg ) { ByteArrayOutputStream baoStream = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream( baoStream ); PrintStream stdOut = System.out; System.setOut( printStream ); parser.parseInput( cmd, arg ); System.setOut( stdOut ); String str = baoStream.toString().trim(); return str; } |
In the above code, getStdout:
- Lines 10-11: Creates a ByteArrayOutputStream wrapped in a PrintStream.
- Line 12: Saves the system stdout object.
- Line 13: Replaces the system stdout object with the PrintStream/ByteArrayOutputStream.
- Line 15: Passes a command and argument to the InputParser object for execution.
- Line 16: Restores the original stdout.
- Lines 18-19: Gets and returns the text that was written to stdout by the given command.
The method testEmptyArg:
- Line 4: Passes a command to getStdout for execution, and captures the return value.
- Line 5: Validates the actual value written to stdout agains the given, expected value.
Geting on to testing, let’s quickly dispatch with testing the two constructors and the public getEquation method; note that the test for getEquation is satisfied in the constructor tests, which I assume are self-explanatory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Test public void testInputParser() { InputParser parser = new InputParser(); assertNotNull( parser.getEquation() ); } @Test public void testInputParserEquation() { Equation equation = new Exp4jEquation(); InputParser parser = new InputParser( equation ); assertEquals( equation, parser.getEquation() ); } |
I have one more test for the parseInput(Command, String) method, which is verifying that it throws an IllegalArgumentException when null is passed for the String argument. To do this we use the assertThrows assertion, which we have seen before. This assertion passes when the invoked method throws the given assertion:
1 2 3 4 5 6 7 8 9 | @Test public void testIllegalArgumentException() { Class<IllegalArgumentException> clazz = IllegalArgumentException.class; assertThrows( clazz, () -> parser.parseInput( Command.SET, null ) ); } |
All of our remaining tests verify that the possible commands are executed correctly.
◼ EQUATION
Testing this command is simple enough. Instantiate an InputParser and save the value of the original equation. Execute the EQUATION command, and verify that a new, non-null equation is created.
1 2 3 4 5 6 7 8 9 10 | @Test public void testParseInputEQUATION() { Equation oldVal = parser.getEquation(); parser.parseInput( Command.EQUATION, "" ); Equation newVal = parser.getEquation(); assertNotNull( oldVal ); assertNotNull( newVal ); assertNotEquals( oldVal, newVal ); } |
◼ EXIT, NONE, YPLOT, XYPLOT, OPEN, SAVE
These are the noop commands. They can all be verified with one parameterized test. All we have to do is issue the command and make sure that a successful result is returned.
1 2 3 4 5 6 7 8 | @ParameterizedTest @ValueSource( strings= {"EXIT","NONE","YPLOT","XYPLOT","OPEN","SAVE" } ) public void testParseInputNOOP( String strCommand ) { Command command = Command.toCommand( strCommand ); Result result = parser.parseInput( command, "" ); assertTrue( result.isSuccess() ); } |
◼ INVALID
Testing this command is the same as testing the noops, except we verify that a negative result is returned. I won’t bother inserting the code here; it’s in the GitHub repository if you want to look at it.
◼ START, END, STEP
Each of these commands can be verified with the same logic:
- Issue the command with an invalid value, and verify that a negative result is returned.
- Issue the command with a new value, and verify that the new value sticks.
- Issue the command without an argument, and verify that its current value is printed to stdout.
I have a unit test for each command, but they all call the same helper method to perform the validation. Here’s the code for the START command unit test, and the helper method.
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 testParseInputSTART() { Equation equation = parser.getEquation(); testSetDouble( Command.START, equation::getRangeStart ); } private void testSetDouble( Command cmd, DoubleSupplier getter ) { double oldVal = getter.getAsDouble(); double newVal = oldVal + 1; Result result = parser.parseInput( cmd, "" + newVal ); assertTrue( result.isSuccess() ); assertEquals( newVal, getter.getAsDouble() ); // test invalid value result = parser.parseInput( cmd, "invalidvalue" ); assertFalse( result.isSuccess() ); // test no-arg option testEmptyArg( cmd, "" + newVal ); } |
◼ PARAM, YPLOT, XYPLOT
Testing these commands is essentially the same as testing the START family of commands, except that we’re working strings instead of numbers. Also, there’s no easy way to derive a new value from an old value, so the helper method for these commands has an extra parameter. Here’s the code for the PARAM unit test and the helper 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 | @Test public void testParseInputPARAM() { Equation equation = parser.getEquation(); String newVal = "newParamName"; testSetString( Command.PARAM, newVal, equation::getParam ); } private void testSetString( Command cmd, String newVal, Supplier<String> getter ) { String oldVal = getter.get(); assertNotEquals( oldVal, newVal ); Result result = parser.parseInput( cmd, newVal ); assertTrue( result.isSuccess() ); assertEquals( newVal, getter.get() ); // test invalid input result = parser.parseInput( cmd, "%invalid%" ); assertFalse( result.isSuccess() ); // test no-arg option testEmptyArg( cmd, "" + newVal ); } |
◼ SET
That leaves us with the SET command. Let’s list some cases that we’ll have to test:
- A single variable name (“a”); the variable should be successfully declared and have an initial value of 0.
- Multiple variables names (“a,b,c,d”); each variable should be successfully declared and have an initial value of 0.
- Variations on the above test case, with spaces sprinkled in among the names and commas (“a , b,c, d”). After ignoring spaces, the result should be the same as the previous test case.
- A single variable with an initial value (“a=5”); this should be successful, and the given initial value must be assigned.
- Variations on the above, with spaces (“a = 5”, “a =5”).
- Multiple variables declared with initial values, and miscellaneous spaces (“a=5, b=-6, c = 7”).
- Multiple variables, some with initial values and some without (“a=5,b,c=6”).
- Declarations of variables with invalid names (“&,9a=pi”).
- Declarations of variables with invalid values (“a=%,b=pii”).
- A mixture of valid and invalid declarations (“a,b=6,c,%=5,d=pii”).
So one command left to test, but lots of different ways to test it. My solution to this is four tests, three of them parameterized, summarized as follows:
● testParseVarsWithoutValues
@ParameterizedTest
@ValueSource( strings=
{ "", "p", "p,q", " a, b , c , d ",
"abc", "def"
})
public void testParseVarsWithoutValues( String str )
● testParseVarsWithValues
@ParameterizedTest
@ValueSource( strings=
{ "p=5", "p=5,q=6", " a = 5 , b = 6 , c = 7 , d = 8 ",
"abc = 5 , def = 6 "
})
public void testParseVarsWithValues( String str )
● testParseVarsWithBadNames
@ParameterizedTest
@ValueSource( strings=
{ "%=5", "5=5,6=6", " 5a = 5 , 6b = 6 , ^c = 7 , ^d = 8 ",
"abc% = 5 , de%f = 6 "
})
void testParseVarsWithBadNames( String str )
● testParseVarsWithBadValues
@ParameterizedTest
@ValueSource( strings=
{ "p=.", "p=.,q=%", " a = 5..0 , b = ..6 , c = %7 , d = 8$ ",
"abc = 55.x , def = x 6, ghi = 5 5 jkl = 5 6"
})
public void testParseVarsWithBadValues( String str )
● testParseVarsGoodAndBadSpecs
public void testParseVarsGoodAndBadSpecs()
Here’s the code for one of the parameterized tests, with notes. The code for the remaining tests can be found in the GitHub repository.
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 | @ParameterizedTest @ValueSource( strings= { "p=5", "p=5,q=6", " a = 5 , b = 6 , c = 7 , d = 8 ", "abc = 5 , def = 6 " }) public void testParseVarsWithValues( String str ) { Equation equation = parser.getEquation(); Result result = parser.parseInput( Command.SET, str ); assertTrue( result.isSuccess() ); String[] specs = str.split( "," ); for ( String spec : specs ) { String[] pair = spec.trim().split( "=" ); // sanity check assertEquals( 2, pair.length ); String name = pair[0].trim(); String value = pair[1].trim(); double dValue = Double.parseDouble( value ); Optional<Double> actVal = equation.getVar( name ); assertTrue( actVal.isPresent() ); assertEquals( dValue, actVal.get() ); } } |
- Lines 3-4: This is the array of Strings to be passed, one at a time, to the test code. You have to look carefully at the commas; some of them separate elements of the array, and some are part of the strings that constitute the elements. The first element is “p=5”, the second is “p=5,q=6”, etc.
- Lines 9-11: Passes each element of the array to parseInput for processing, and verifies that it returns a successful result.
- Line 13: Splits the test string at the comma, obtaining an array of strings. For example, the second test string, “p=5,q=6” will be rendered into the array whose first element is “p=5” and whose second element is “q=6”.
- Line 16: Splits each element in the array at the equal sign. By design, every input string is a valid declaration, and each declaration includes an initial value, therefor every string should be split into a two element array such as {“p”, “6”}.
- Line 18: Sanity check; verifies that the input string was divided as expected.
- Lines 19-20: Trims the strings in the array.
- Line 21: Converts the string in the second element of the array to a double value. Once again, by design this should be a valid double value, so the conversion should not fail.
- Line 22: Obtains the actual value of the variable from the equation encapsulated in the InputParser object. Recall that the Equation.getVar(String name) method returns an Optional object. If name matches a declared variable, the Optional contains the declared value; if name is not a declared variable, the Optional will be empty.
- Lines 23-24: Verifies that the getVar method returned a successful result.
🔳 public class Exp4jEquation implements Equation
◼ Initialization
For the sake of convenience, the tester for this class begins with an instance variable of type Exp4jEquation, which is initialized in a @BeforeEach method:
private Exp4jEquation equation;
@BeforeEach
public void beforeEach() throws Exception
{
equation = new Exp4jEquation();
}
And we have some private helper methods which are mainly intended to support the constructor tests:
● private void validateDefaultYExpression()
This method verifies that the default y-expression, y=1, has been set in an equation. To do this it plots three points, and verifies that the y value of each is 1:
private void validateDefaultYExpression()
{
equation.setRange( 1, 3, 1 );
equation.yPlot().forEach(
p -> assertEquals( 1, p.getY(), "Y" )
);
}
● private void validateDefaultXExpression()
This method verifies that the default x-expression, x=1, has been set in an equation. To do this it plots three points in a parametric equation (xyPlot), and verifies that the x value of each is 1.
● private void validateDefaultRange()
We don’t have an explicit default range, but we can at least verify that the range is sane: end ≤ start and step > 0.
● private void validateDefaultVariables()
Verifies that the expected default variables (see specification in the design lesson) are present in an equation.
◼ Constructors
Validating the constructors is mainly an exercise in invoking the above three methods. Here’s the code for testing the two-parameter constructor, Exp4jEquation( Map<String,Double> vars, String expr ), which has to check for some of the defaults, plus the variables declared in the vars map, and the presence of the expression expr. To verify the expression it sets the expression and range to values that let us easily predict the output of xyPlot, then invokes xyPlot:
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 | public void testExp4jEquationMapOfStringDoubleString() { Map<String,Double> mapIn = new HashMap<>(); String[] vars = { "h", "j", "k", "l" }; for ( String str : vars ) mapIn.put( str, (double)str.charAt( 0 ) ); equation = new Exp4jEquation( mapIn, "2t" ); validateDefaultVariables(); validateDefaultRange(); // validate mapped variables declared Map<String,Double> actMap = equation.getVars(); for ( String var : vars ) { Double val = actMap.get( var ); assertNotNull( val ); assertEquals( val, (double)var.charAt( 0 ) ); } // validate expressions equation.setRange( 1, 1, 1 ); equation.xyPlot().forEach( p -> { assertEquals( 1, p.getX(), "X" ); assertEquals( 2, p.getY(), "Y" ); } ); } |
◼ Additional Tests
Many of the additional tests are fairly simple:
● newEquation()
Call equation.newEquation() and verify that it returns a new equation with all defaults properly set.
● setVar(String name, double val), getVar(String name), removeVar(String name)
Set a variable, and verify that it was declared with the correct value. Remove the variable and verify that it’s gone.
● getVars()
Set some variables, get the map returned by getVars and verify that all the variables are present with the correct values. One quirk of this test method deserves mention. Each value of each test variable name is the Unicode value of its first character: equation.setVar( var, var.charAt( 0 ) )
making it easy to verify that the variable is present with the correct value: Double actVal = actVars.get( var );
assertNotNull( actVal );
assertEquals( var.charAt( 0 ), actVal );
● setXExpression( String expr ),setYExpression( String expr ), getXExpression(), getYExpression()
I have two pairs of tests for these methods: testSetXExpression, testSetXExpressionGoWrong, testSetYExpression and testSetYExpressionGoWrong. The GoWrong methods verify that an invalid expression returns a failed status. The GoWrong methods also verify that the original value of the expression is unchanged. Here are the two methods for setXExpression.
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 | @Test public void testSetXExpression() { double xier = 2; String xExpr = xier + "t"; Result result = equation.setXExpression( xExpr ); assertTrue( result.isSuccess() ); assertEquals( xExpr, equation.getXExpression() ); equation.setRange( 1, 1, 1 ); equation.xyPlot() .forEach( p -> assertEquals( p.getX(), xier ) ); // try setting an invalid expression result = equation.setXExpression( "invalid" ); assertFalse( result.isSuccess() ); } @Test public void testSetXExpressionGoWrong() { String oldXExpr = equation.getXExpression(); String xExpr = "notAVar * x"; Result result = equation.setXExpression( xExpr ); assertFalse( result.isSuccess() ); assertEquals( oldXExpr, equation.getXExpression() ); } |
● setParam( String name ), getParam(), setRange( double start, double end, double step ), setRangeStart( double start ), g setRangeEnd( double end ), getRangeStart(),etRangeEnd(), setRangeStep( double step ), getRangeStep()
These are simple tests for getters and setters.
● evaluate( String exprStr )
I have two unit tests for this method, one for testing valid expressions and one for testing invalid expressions. The test for invalid expressions is a simple parameterized test. Parameterizing the test for valid expressions was not quite so easy, because, for each test string, the correct value had to be specified. So the test for valid expressions comes with a private helper method. The code follows.
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 | @Test public void testEvauatePass() { testEvaluatePass( "2", 2 ); testEvaluatePass( "-.1", -.1 ); testEvaluatePass( "2 * 3", 6 ); testEvaluatePass( ".3^2", .09 ); testEvaluatePass( "2pi", 2 * Math.PI ); testEvaluatePass( "sin(pi/2)", 1 ); testEvaluatePass( "log(e)", 1 ); } private void testEvaluatePass( String expr, double expVal ) { Optional<Double> optional = equation.evaluate( expr ); assertTrue( optional.isPresent(), expr ); assertEquals( expVal, optional.get(), .0001, expr ); } @ParameterizedTest @ValueSource(strings={ "a", "2x", "x^2", "cos(t)" } ) public void testEvaluateFail( String str ) { Optional<Double> optional = equation.evaluate( str ); assertFalse( optional.isPresent() ); } |
● yPlot(), xyPlot()
Each of these methods gets a pair of test methods: testYPlot, testYPlotGoWrong, testXYPlot and testXYPlotGoWrong. The GoWrong tests verify that an exception is thrown when trying to generate a plot from an invalid expression. To get the invalid expression I had to:
- Set a variable;
- Set an expression dependent on that variable;
- Remove the variable; and
- Attempt to generate a plot.
Here is the code for testYPlot and testYPlotGoWrong.
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 | @Test public void testYPlot() { double xier = 2; String yExpr = xier + "x"; equation.setYExpression( yExpr ); double start = -2; double end = 2; double step = .1; List<Point2D> expPoints = DoubleStream.iterate( start, x -> x <= end, x -> x + step ) .mapToObj( x -> new Point2D.Double( x, xier * x ) ) .collect( Collectors.toList() ); equation.setRange( start, end, step ); List<Point2D> actPoints = equation.yPlot() .collect( Collectors.toList() ); assertEquals( expPoints, actPoints ); } @Test public void testYPlotGoWrong() { String varName = "varName"; String yExpr = varName + " + x"; equation.setVar( varName, 0 ); equation.setYExpression( yExpr ); equation.removeVar( varName ); Class<ValidationException> clazz = ValidationException.class; assertThrows( clazz, () -> equation.yPlot() ); } |
● isValidName( String name ), isValidValue( String valStr )
Each of these methods gets a pair of parameterized unit tests that verify they succeed when passed a valid value, and fail when passed an invalid value. Here is one pair of tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @ParameterizedTest @ValueSource(strings={ "0", "0.1", "0.", "-.1", "-1.1", "pi", "cos(pi)" } ) public void testIsValidValueTrue( String str ) { assertTrue( equation.isValidValue( str ), str ); } @ParameterizedTest @ValueSource(strings={ "a", "2x", "x^2", "cos(t)" } ) public void testIsValidValueFalse( String str ) { assertFalse( equation.isValidValue( str ), str ); } |
Summary
This concludes testing for the user input module of the Cartesian plane project. Some of the new topics we covered included:
- Testing equals(Object) and hashCode() overrides.
- How to generate input data for reading using a BufferedReader.
- How to validate data written to stdout.
One subject of particular interest was how to elegantly test data gathering from an input source. In CommandReaderTest we used a technique that involved pairs of unit test methods (for example, public void testSimpleCommandWithoutArg() and private void testSimpleCommandWithoutArg( BufferedReader reader ), and a utility method (ioTest):
- The public method generated the test data, and passed it to ioTest along with a reference to the private method.
- The utility method (ioTest) generated an input source from the test data (a ByteArrayInputStream wrapped in a BufferedReader) using a try-with-resources block. The input source was then passed to the private method.
- The private method read the input data and performed the relevant testing.
- When the private method returned to ioTest the try-with-resources block exited, automatically disposing of the input source.
In the next lesson we will extend our user input module to include configuring and plotting polar coordinates, and introducing the first bits of a graphical user interface (GUI).
Next:
Cartesian Plane Lesson 12: User Input Extensions, Introduction to GUIs