This page will discuss the ProfileParserTest class, a JUnit test class for validating the ProfileParser class. We’ll also learn about a new JUnit feature, the @Timeout annotation.
GitHub repository: Cartesian Plane Lesson 18
Previous lesson: Cartesian Plane Lesson 18 Page 4: ProfileParser
Class ProfileParserTest
The ProfileParser class encapsulates a facility for saving Profile data to a file and restoring it from a file. This class validates its operation. Input to the parser is formatted as strings, one string per line, each representing a name/value pair. If the parser encounters an error in a string, it reports an error and continues processing the remaining strings. It concentrates on the following test categories:
- The ability to correctly write a Profile to a stream.
- The ability to correctly read Profile data from a stream and incorporate it into an existing Profile.
- The ability to detect and report syntax errors in an input stream.
- The ability to continue processing well-formatted input after encountering a syntax error.
One of the challenges in this unit test is to handle error dialogs. The problem is twofold:
⬛ 1. Detecting and recovering from syntax errors.
When a syntax error is encountered in an input stream, the ProfileParser is expected to report the error and continue processing the stream. Consider, for example, the following input stream:
PROFILE default
gridUnit 65.0
CLASS LinePropertySetAx
color 0x000000
stroke 2.0
class GraphPropertySetMW
bgColor 0xe6e6e6
fgColor 0x000000
The ProfileProcessor should successfully parse the first two lines. On the third line, it should post an error dialog because LinePropertySetAx is not a valid class name. It should also post error dialogs for the following two lines because, while they may be valid property name/value pairs, we don’t know which LinePropertySet to apply them to. After posting three error dialogs, the ProfileParser should be able to parse the final three lines of input successfully.
To manage this category of test, we use the expectDialog(Runnable operation) method, which performs as follows:
- Start the operation in a dedicated thread (threadA).
- As long as threadA is alive:
- Pause for a quarter second (Utils.pause(250)).
- Check for a visible error dialog. If a visible dialog is found:
- Push the dialog’s OK button.
- Wait for the dialog to lose visibility.
The expectDialog method returns the number of dialogs it encounters. If we run a test that parses the sample input stream above, we can verify that three errors were detected and reported.
⬛ 2. Encountering unexpected syntax errors.
If we run a test on what we believe to be well-formed input data, we may still encounter an error because of a bug in the ProfileParser or our test code. In this case, we may wait forever for an unexpected error dialog to be dismissed. To handle this problem, we will use the JUnit @Timeout tag to limit the maximum time allotted for a given test. Then, instead of waiting for the error dialog to be dismissed, the test will fail with a timeout error.
Note: Another way to handle this potential problem is to execute a test consistently using the expectDialog method. Then, to validate the operation, we could verify that expectDialog returned 0, indicating that no dialogs were posted. But doing it by imposing time limits enables us to introduce the JUnit @Timeout tag.
ProfileParserTest Class Implementation
Following is a discussion of the ProfileParserTest class implementation. This test has no overwhelming GUI issues, as we’ve seen, for example, in PlotPanelTest. So, we don’t need an auxiliary class (such as PlotPanelTestGUI) to manage graphics components and EDT scheduling. In addition to the principal test methods, we have an inner class and the usual class/instance variables and helper methods to discuss. We’ll begin our discussion with those.
Infrastructure
Following is a discussion of the inner class, class/instance variables, and helper methods we use to support testing.
⬛ private class Mutator
This inner class assists with accessing float property values in the LinePropertySet objects. An object of this class has a setter (type BiConsumer<LinePropertySet, Float>) and a getter (type Function<LinePropertySet, Float>) for a specific property in a given LinePropertySet class. To transfer the value of a property from one Profile to another, we might use logic like this:
private void getDistinctVal( Mutator mutator )
{
// propSetName will be "LinePropertySetAxes," for example.
String propSetName = mutator.propSetName;
// Profile/LinePropertySet to transfer from
LinePropertySet source =
distinctProfile.getLinePropertySet( propSetName );
// Profile/LinePropertySet to transfer to
LinePropertySet dest =
workingProfile.getLinePropertySet( propSetName );
float val = mutator.getter.apply( source );
mutator.setter.accept( dest, val );
}
⬛ Mutator Instance Variables
Here is an annotated list of instance variables declared in the inner class Mutator.
1 2 3 4 5 6 7 | private class Mutator { public final String propSetName; public final String propName; public final Function<LinePropertySet,Float> getter; public final BiConsumer<LinePropertySet,Float> setter; // ... |
- Line 3: This is the encapsulated LinePropertySet class name. As seen in the above sample code (see getDistinctVal) this variable is used to designate one of the LinePropertySet objects in a Profile. It is also used in creating a CLASS declaration in a Profile output stream, for example:
String decl = ProfileParser.CLASS + " " + mutator.propSetName; - Line 4: This is the name of the encapsulated property. It can be used to add a name/value pair to a Profile input stream, for example:
LinePropertySet propSet =
distinctProfile.getLinePropertySet( mutator.propSetName );String prop =mutator.propName + " " + mutator.getter.apply( propSet ); - Lines 5,6: The getter and setter for the encapsulated property.
⬛ Mutator Constructor
The constructor for the Mutator class initializes the instance variables. It looks like this:
public Mutator(
String propSetName,
String propName,
Function<LinePropertySet, Float> getter,
BiConsumer<LinePropertySet, Float> setter
)
{
super();
this.propSetName = propSetName;
this.propName = propName;
this.getter = getter;
this.setter = setter;
}
Here’s an example of invoking the constructor:
new Mutator(
LinePropertySetAxes.class.getSimpleName(),
ProfileParser.LENGTH,
p -> p.getLength(),
(p,v) -> p.setLength( v )
);
⬛ Mutator Public Method: public void add( List<String> list )
The Mutator class’s sole public method is used to a) add a CLASS declaration to a list of strings, for example: CLASS LinePropertySetTicMinor
b) Add a name/value pair to the same list of strings, for example: length 6.0
And c) Copy the value declared in (b) to the Profile that will be used later in test validation.
Here is the annotated code for the add method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public void add( List<String> list ) { LinePropertySet dstPropSet = workingProfile.getLinePropertySet( propSetName ); LinePropertySet srcPropSet = distinctProfile.getLinePropertySet( propSetName ); float distinctVal = getter.apply( srcPropSet ); setter.accept( dstPropSet, distinctVal ); String classDecl = ProfileParser.CLASS + " " + propSetName; String valueDecl = propName + " " + distinctVal; list.add( classDecl ); list.add( valueDecl ); } |
- Lines 3,4: Get the designated LinePropertySet from the working Profile (the destination LinePropertySet).
- Lines 5,6: Get the designated LinePropertySet from the Profile that contains unique values for all Profile properties (the source LinePropertySet).
- Lines 7,8: Get a distinct value for the target property.
- Line 9: Save the distinct value in the working Profile for later reference when we use logic like this:
Stream the list of strings.
Parse the stream with ProfileParser to create testProfile.
assertEquals(workingProfile, testProfile). - Lines 11,12: Assemble the class declaration and name/value pair.
- Lines 13,14: Add the class declaration and name/value pair to the list of strings.
⬛ ProfileParserTest Instance Variables
Following is an annotated list of the ProfileParserTest class’s instance variables.
1 2 3 4 5 6 7 8 9 10 | class ProfileParserTest { private final Profile protoProfile = new Profile(); private final Profile distinctProfile = ProfileUtils.getDistinctProfile( protoProfile ); private Profile workingProfile; private JDialog errorDialog = null; private AbstractButton errorDialogOKButton; // ... |
- Line 3: This is our prototype Profile. It is created when the program begins execution and is never modified, so all its properties are initialized to those stored by the PropertyManager before any modifications are made. In our afterEach method, it is written back to the PropertyManager, ensuring that the next test begins with the PropertyManager in its original state.
- Lines 4,5: This Profile contains values for all properties guaranteed to be distinct from those stored in the PropertyManager. It is created at the start of the test and never modified.
- Line 7: The working Profile, to be used as needed by individual tests. It is returned to its initial state in the beforeEach method.
- Line 8: This is a reference to the dialog that displays error messages during input processing. It is initialized every time we expect an error message to be displayed (see expectDialog and getDialogAndOKButton).
- Line 9: The OK button from the error dialog. It is initialized every time errorDialog is initialized (see expectDialog and getDialogAndOKButton).
🟦 @BeforeEach, @AfterEach Methods
The before-each and after-each methods ensure that the ProfileManager and workingProfile are in a consistent state at the start of every test. They look like this:
@BeforeEach
public void beforeEach() throws Exception
{
workingProfile = new Profile();
}
@AfterEach
public void afterEach()
{
protoProfile.apply();
}
⬛ Private Methods
Below, we discuss the private methods used to support test operations in our ProfileParserTest class.
🟦 private List getTestPropertyList()
Assembles a list of strings to be streamed for input testing. The list contains:
- A PROFILE declaration incorporating a unique name.
- A GRID_UNIT name/value pair using a unique value.
- A CLASS declaration for the GraphPropertySetMW.
- One name/value pair for a property in the GraphPropertySet using a unique value.
- A CLASS declaration for a LinePropertySet.
- One name/value pair for a property in the LinePropertySet using a unique value.
The output list looks like this (without the leading [n]):
[0] PROFILE default_MUTATE
[1] gridUnit 66.0
[2] class GraphPropertySetMW
[3] font_size 20.0
[4] class LinePropertySetAxes
[5] stroke 12.0
Each property value in the list is also recorded in the workingProfile for later test validation; see, for example, testExcessWhitespace and testMissingClassDecl below. Important: the ordering of the strings in the output list must be as shown above. Some tests will obtain this list and insert additional strings at specific locations. See, for example, testExcessWhitespace and testMissingClassDecl below.
The annotated listing for this method 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 | private List<String> getTestPropertyList() { String name = distinctProfile.getName(); float gridUnit = distinctProfile.getGridUnit(); String lineName = LinePropertySetAxes.class.getSimpleName(); String graphName = GraphPropertySetMW.class.getSimpleName(); float fontSize = distinctProfile.getMainWindow().getFontSize(); float stroke = distinctProfile.getLinePropertySet( lineName ).getStroke(); workingProfile.setName( name ); workingProfile.setGridUnit( gridUnit ); workingProfile.getMainWindow().setFontSize( fontSize ); workingProfile.getLinePropertySet( lineName ).setStroke( stroke ); List<String> props = new ArrayList<>(); props.add( ProfileParser.PROFILE + " " + name ); props.add( ProfileParser.GRID_UNIT + " " + gridUnit ); props.add( ProfileParser.CLASS + " " + graphName ); props.add( ProfileParser.FONT_SIZE + " " + fontSize ); props.add( ProfileParser.CLASS + " " + lineName ) ; props.add( ProfileParser.STROKE + " " + stroke ); return props; } |
- Lines 3-10: Gather the data that will be used to populate the output list.
- Line 3: A unique name for the PROFILE declaration.
- Line 4: A unique value for a GRID_SIZE declaration.
- Line 5: The class name for the CLASS declaration preceding the LinePropertySet property declarations.
- Line 6: The class name for the CLASS declaration preceding GraphPropertySet property declarations.
- Lines 7,8: A unique value for a FONT_SIZE declaration.
- Lines 9,10: A unique value for a STROKE declaration.
- Lines 12-15: Copy the unique values to the workingProfile.
- Lines 17-23: Create a list of strings:
- Line 18: Add the PROFILE declaration to the list.
- Line 19: Add the GRID_SIZE declaration to the list.
- Line 20: Add the GraphPropertySetMW class declaration to the list.
- Line 21: Add the FONT_SIZE declaration to the list.
- Line 22: Add the LinePropertySetAxes class declaration to the list.
- Line 23: Add the STROKE declaration to the list.
- Line 25: Return the list.
An example of using the getTestPropertyList method might look like this:
List<String> props = getTestPropertyList();
ProfileParser testParser = new ProfileParser();
testParser.loadProperties( props.stream() );
Profile testProfile = testParser.getProfile();
assertEquals( workingProfile, testProfile );
🟦 private String excessWhitespace( String nvPair )
This helper method assists in testing input operations where extra whitespace should be ignored. Given a valid name/value pair, this method generates a string representation of the pair with extra whitespace added. For example, if the input is {“gridUnit”,”66.0″} the output will be: " gridUnit 66.0 "
The code for this method follows.
private String excessWhitespace( String nvPair )
{
final String whitespaceStr = " \t ";
String[] tokens = nvPair.split( "\\s+" );
String name = tokens[0];
String value = tokens[1];
StringBuilder bldr = new StringBuilder()
.append( whitespaceStr )
.append( name )
.append( whitespaceStr )
.append( value )
.append( whitespaceStr );
return bldr.toString();
}
🟦 private void addFontSize( List<String> list )
This is a helper method for the testLoadProperties test method. It adds to a list of strings the declarations necessary to specify a font size in an input stream. The declarations include the GraphPropertySet class declaration and the FONT_SIZE property declaration: class GraphPropertySetMW
font_size 10.0
The font size value is taken from distinctProfile and recorded in workingProfile for later validation. Here’s the code.
private void addFontSize( List<String> list )
{
String propSetName =
GraphPropertySetMW.class.getSimpleName();
GraphPropertySet srcPropSet =
distinctProfile.getMainWindow();
GraphPropertySet dstPropSet =
workingProfile.getMainWindow();
float distinctVal = srcPropSet.getFontSize();
dstPropSet.setFontSize( distinctVal );
String classDecl =
ProfileParser.CLASS + " " + propSetName;
String propDecl =
ProfileParser.FONT_SIZE + " " + distinctVal;
list.add( classDecl );
list.add( propDecl );
}
🟦 private void getDialogAndOKButton()
This method attempts to find a visible message dialog. If it succeeds, it locates the dialog’s OK button and sets the errorDialog and errorDialogOKButton instance variables. If it fails to find the dialog, these variables are set to null. The logic is executed on the EDT. Here’s the code.
private void getDialogAndOKButton()
{
final boolean canBeFrame = false;
final boolean canBeDialog = true;
final boolean mustBeVis = true;
GUIUtils.schedEDTAndWait( () -> {
errorDialogOKButton = null;
errorDialog = null;
ComponentFinder finder =
new ComponentFinder( canBeDialog, canBeFrame, mustBeVis );
Window window = finder.findWindow( c -> true );
if ( window != null )
{
assertTrue( window instanceof JDialog );
errorDialog = (JDialog)window;
Predicate<JComponent> pred =
ComponentFinder.getButtonPredicate( "OK" );
JComponent comp =
ComponentFinder.find( window, pred );
assertNotNull( comp );
assertTrue( comp instanceof AbstractButton );
errorDialogOKButton = (AbstractButton)comp;
}
});
}
🟦 private void okAndWait()
This method must only be called when errorDialog and errorDialogOKButton are non-null (see getDialogAndOKButton above). It pushes the OK button and waits for the dialog to lose visibility. Before returning, it sets the errorDialogOKButton to null. The listing for this method follows.
private void okAndWait()
{
errorDialogOKButton.doClick();
errorDialogOKButton = null;
while ( errorDialog.isVisible() )
Utils.pause( 125 );
}
🟦 private int expectDialog( Runnable runner )
This method is invoked when an operation is expected to raise one or more error dialogs. It starts the operation (runner) in a dedicated thread and waits for the thread to reach the terminated state. Periodically, it checks for a dialog to be posted. If a dialog is posted, it is dismissed. The method returns the total number of dialogs encountered. An example of using this method can be found in testNVPairWrongTokenCount, which works something like this:
List<String> props = ...
// Create a list of properties, two of which contain an incorrect
// number of tokens, such as:
// font_size
// font_size 10 extraArg
ProfileParser testParser = new ProfileParser();
int testCount = expectDialog( () ->
testParser.loadProperties( props.stream() )
);
assertEquals( 2, testCount );
The annotated code for this method follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private int expectDialog( Runnable runner ) { Thread thread = new Thread( runner ); int dialogCounter = 0; thread.start(); while ( thread.isAlive() ) { Utils.pause( 250 ); getDialogAndOKButton(); if ( errorDialogOKButton != null ) { ++dialogCounter; okAndWait(); } } return dialogCounter; } |
- Line 1: The runner parameter is the operation to execute, for example:
() -> testParser.loadProperties( props.stream() ) - Line 3: Create the dedicated thread to execute the given operation.
- Line 4: Declare the dialog counter.
- Line 5. Start the dedicated thread.
- Line 6: As long as the dedicated thread is alive:
- Line 8: Pause.
- Line 9: Look for a visible message dialog. If one is found, getDialogAndOKButton will set errorDialogOKButton to a non-null value (see above).
- Line 10: If a visible dialog is found:
- Line 11: Update the dialog counter.
- Line 12: Press the dialog’s OK button and wait for the dialog to lose visibility.
- Line 16: Return the dialog count.
About the JUnit @Timeout Annotation
The JUnit @Timeout(n) tag limits the execution time of a test method to n seconds, where n is a long value. To specify a timeout in a unit other than seconds, use the syntax @Timeout(value=n,unit=time-unit), where n is a long integer, and time-unit is one of the enumerated constants from java.util.concurrent.TimeUnit:
- TimeUnit.DAYS
- TimeUnit.HOURS
- TimeUnit.MINUTES
- TimeUnit.SECONDS
- TimeUnit.MILLISECONDS
- TimeUnit.MICROSECONDS
- TimeUnit.NANOSECONDS
For example, to specify a timeout of one-half second, you could use: @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
You can apply a timeout to an individual test method: @Timeout(5)
@Test
public void testMisplacedPropertyMisc()...
To apply a timeout to all test methods in a test class, place the timeout annotation on the test class declaration: @Timeout( 2 ) public class ProfileParserTest
If you declare a timeout for both a class and a specific method, the method declaration takes precedence. In our ProfileParserTest class, we will declare a 2-second timeout on the class declaration, which is suitable for all of our test methods except testMisplacedPropertyMisc, for which we will need a 5-second timeout.
Test Class Declaration, Principal Test Methods
Below, find a discussion of the ProfileParserTest class’s declaration and test methods.
⬛ ProfileParserTest Class Declaration
Our test class is declared with a @Timeout declaration, which limits the execution time of all tests to 2 seconds unless otherwise specified; see also method testMisplacedPropertyMisc. It looks like this:
@Timeout( 2 )
public class ProfileParserTest
{
// ...
}
⬛ public void testProfileParser()
This is a simple test for the default constructor. The constructor should instantiate a Profile with values based on those in the PropertyManager. Here’s the code.
@Test
public void testProfileParser()
{
// Default constructor instantiates a profile that is expected
// to be initialized with default values from the PropertyManager.
ProfileParser parser = new ProfileParser();
Profile profile = parser.getProfile();
assertNotNull( profile );
assertEquals( protoProfile, profile );
}
⬛ public void testProfileParserProfile()
This is a simple test for the constructor that requires a Profile argument. The instantiated object should use the given Profile as its encapsulated member. We provide the working profile a unique value and use it to instantiate a ProfileParser, then verify that the ProfileParser encapsulates a Profile containing the given value. A listing of this method follows:
Note: In this method, we encounter the JUnit assertSame(expected, actual) assertion. This method asserts that expected and actual are identical, i.e., expected == actual.
@Test
public void testProfileParserProfile()
{
float diffGridUnit = distinctProfile.getGridUnit();
workingProfile.setGridUnit( diffGridUnit );
ProfileParser parser =
new ProfileParser( workingProfile );
Profile testProfile = parser.getProfile();
assertSame( workingProfile, testProfile );
assertEquals( diffGridUnit, testProfile.getGridUnit() );
}
⬛ public void testGetProperties()
This method primarily intends to test the ProfileParser’s output method, getProperties, which generates a formatted output stream from its encapsulated Profile. In doing so, it also exercises the ProfileParser’s input logic (loadProperties), making the output logic test dependent on the validity of the input logic. It generates a stream from distinctProfile, then parses it and verifies that the input data is equal to that stored in distinctProfile. The method looks like this:
@Test
public void testGetProperties()
{
ProfileParser outParser = new ProfileParser( distinctProfile );
Stream<String> stream = outParser.getProperties();
ProfileParser inParser = new ProfileParser();
inParser.loadProperties( stream );
assertEquals( distinctProfile, inParser.getProfile() );
}
⬛ public void testLoadProperties()
This test exercises the ProfileParser’s loadProperties method. We provide it with an input stream with the following characteristics:
- All property values used in the test come from distinctProfile, so they’re guaranteed to differ from those in workingProfile, which was initialized in the beforeEach method.
- It does not include all Profile properties. Part of this test is to verify that the specified properties are merged with default properties.
- It includes the name property obtained directly from the Profile class.
- It includes one property from each of the Profile’s encapsulated GraphPropertySet and LinePropertySet objects.
Below, find an annotated listing of the code for this method. See also the Mutator class.
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 | public void testLoadProperties() { Mutator[] mutators = { new Mutator( LinePropertySetAxes.class.getSimpleName(), ProfileParser.LENGTH, p -> p.getLength(), (p,v) -> p.setLength( v ) ), new Mutator( LinePropertySetGridLines.class.getSimpleName(), ProfileParser.STROKE, p -> p.getStroke(), (p,v) -> p.setStroke( v ) ), new Mutator( LinePropertySetTicMajor.class.getSimpleName(), ProfileParser.SPACING, p -> p.getSpacing(), (p,v) -> p.setSpacing( v ) ), new Mutator( LinePropertySetTicMinor.class.getSimpleName(), ProfileParser.STROKE, p -> p.getStroke(), (p,v) -> p.setStroke( v ) ), }; List<String> list = new ArrayList<>(); String distinctName = distinctProfile.getName(); list.add( ProfileParser.PROFILE + " " + distinctName ); workingProfile.setName( distinctName ); addFontSize( list ); for ( Mutator mutator : mutators ) mutator.add( list ); ProfileParser testParser = new ProfileParser(); testParser.loadProperties( list.stream() ); assertEquals( workingProfile, testParser.getProfile() ); } |
- Lines 3-29: Create an array of Mutator objects that will be used to add LinePropertySet properties to the input stream:
- Lines 5-10: Instantiate a Mutator to exercise the LENGTH property in the LinePropertySetAxes object, specifying:
- The name of the LinePropertySet subclass to be exercised;
- The name of the property to be exercised;
- A getter for the property to be exercised and
- A setter for the property to be exercised.
- Lines 11-16: Instantiate a Mutator to exercise the STROKE property in the Profile’s LinePropertySetGridLines object.
- Lines 17-22: Instantiate a Mutator to exercise the SPACING property in the Profile’s LinePropertySetTicMajor object.
- Lines 23-28: Instantiate a Mutator to exercise the STROKE property in the Profile’s LinePropertySetTicMinor object.
- Lines 5-10: Instantiate a Mutator to exercise the LENGTH property in the LinePropertySetAxes object, specifying:
- Line 30: Create a list to hold the output of the method.
- Line 31: Get a unique profile name.
- Line 32: Add the PROFILE declaration to the list of strings.
- Line 33: Copy the unique profile name to the workingProfile for later validation.
- Line 35: Add the declarations needed to set the font size property to the list (see addFontSize).
- Lines 37,38: Call the add method for each Mutator instantiated above. The add method will a) add to the list of strings a declaration for the encapsulated LinePropertySet, b) add to the list of strings a declaration for the encapsulated property, and c) copy the value of the property to the workingProfile for later validation. See the Mutator.add() method above.
- Line 40: Create a ProfileParser.
- Line 41: Use the ProfileParser to parse the stream generated from the list.
- Line 42: Verify that the Profile obtained by parsing the input stream equals the workingProfile.
🟦 public void testExcessWhitespace()
This method verifies that the ProfileParser ignores blank lines and excess whitespace when processing input. It creates the list shown below and then uses the list as an input stream:
[0] " "
[1] " PROFILE default_MUTATE "
[2] " gridUnit 66.0 "
[3] " "
[4] " class GraphPropertySetMW "
[5] " font_size 20.0 "
[6] " class LinePropertySetAxes "
[7] " stroke 12.0 "
[8] " "
Following 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 | @Test public void testExcessWhitespace() { List<String> props = getTestPropertyList(); int len = props.size(); for ( int inx = 0 ; inx < len ; ++inx ) { String newString = excessWhitespace( props.get( inx ) ); props.set( inx, newString ); } props.add( 0, " " ); props.add( len / 2, " " ); props.add( " " ); ProfileParser testParser = new ProfileParser(); testParser.loadProperties( props.stream() ); Profile testProfile = testParser.getProfile(); assertEquals( workingProfile, testProfile ); } |
- Line 4: Get a list of strings to use as an input stream (see getTestPropertyList for the ordered list of strings).
- Lines 5-10: Add excess whitespace to every string in the list (see excessWhitespace).
- Line 11: Add a blank line at the start of the list.
- Line 12: Add a blank line in the middle of the list.
- Line 13: Add a blank line at the end of the list.
- Line 14: Create a parser. This will have an encapsulated profile that is equivalent to protoProfile.
- Line 15: Load the properties from the input stream generated from the list.
- Line 16: Get the Profile encapsulated in testParser.
- Line 17: Verify that the workingProfile is equal to the testProfile.
🟦 public void testNVPairWrongTokenCount()
This method tests the ProfileParser’s ability to detect and report errors caused by invalid name/value pairs. Here is an annotated listing of this method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public void testNVPairWrongTokenCount() { List<String> props = getTestPropertyList(); props.add( 3, ProfileParser.FONT_SIZE + " " ); props.add( 4, ProfileParser.FONT_SIZE + " " + 10 + ", a" ); ProfileParser testParser = new ProfileParser(); int testCount = expectDialog( () -> testParser.loadProperties( props.stream() ) ); assertEquals( 2, testCount ); Profile testProfile = testParser.getProfile(); assertEquals( workingProfile, testProfile ); } |
- Line 3: Get a list of strings to use as an input stream.
- Line 4: Add to the list a line with only one token (error #1).
- Line 5: Add to the list a line with too many tokens (error #2).
- Line 7: Create a ProfileParser.
- Lines 8-11: Invoke expectDialog, which will:
- Line 9: Generate a stream from the list of strings and then process it by invoking testParser.loadProperties.
- Line 10: Return the number of dialogs posted during the invocation of testParser.loadProperties.
- Line 12: Verify that loadProperties detected and both errors.
- Line 13: Get the Profile encapsulated in testParser.
- Line 14: Verify that all input lines were successfully processed except for the two lines with errors.
🟦 public void testMissingClassDecl()
This method tests the ProfileParser’s ability to detect and report errors caused by GraphPropertySet and LinePropertySet properties that are declared without a preceding CLASS directive. The list that it generates for parsing is shown below:
[0] PROFILE default_MUTATE
[1] gridUnit 66.0
[2] font_size 10
[3] stroke 2
[4] class GraphPropertySetMW
[5] font_size 20.0
[6] class LinePropertySetAxes
[7] stroke 12.0
Following is the code for testMissingClassDecl.
public void testMissingClassDecl()
{
List<String> props = getTestPropertyList();
props.add( 2, ProfileParser.FONT_SIZE + " " + 10 );
props.add( 3, ProfileParser.STROKE + " " + 2 );
ProfileParser testParser = new ProfileParser();
int testCount = expectDialog( () ->
testParser.loadProperties( props.stream() )
);
assertEquals( 2, testCount );
Profile testProfile = testParser.getProfile();
assertEquals( testProfile, workingProfile );
}
🟦 public void testMisplacedProperty()
This is another error processing test. This test generates a stream in which a GraphPropertySet property is placed after a LinePropertySet class directive, and a LinePropertySet property is placed after a GraphPropertySet class directive. Here’s a listing for this method.
public void testMisplacedProperty()
{
List<String> props = getTestPropertyList();
props.add( 3, ProfileParser.STROKE + " " + 2 );
// line property class declaration is now at line 5.
props.add( 6, ProfileParser.FONT_SIZE + " " + 10 );
ProfileParser testParser = new ProfileParser();
int testCount = expectDialog( () ->
testParser.loadProperties( props.stream() )
);
assertEquals( 2, testCount );
Profile testProfile = testParser.getProfile();
assertEquals( testProfile, workingProfile );
}
🟦 public void testInvalidClassDecl()
This method verifies that our code reacts correctly when encountering an invalid CLASS directive. It obtains an ordered property list (see getTestPropertyList), then, at line 4 (after font_size and before class LinePropertySetAxes), it adds three strings: an invalid class declaration followed by two property declarations:
CLASS notAValidClassName
stroke 3
font_size 10
When we stream the list to the loadProperties method: testParser.loadProperties( props.stream() )
we expect to get three error messages. The code follows.
@Test
public void testInvalidClassDecl()
{
List<String> props = getTestPropertyList();
props.add( 4, ProfileParser.CLASS + " " + "notAValidClassName" );
props.add( 5, ProfileParser.STROKE + " " + 2 );
props.add( 6, ProfileParser.FONT_SIZE + " " + 10 );
ProfileParser testParser = new ProfileParser();
int testCount = expectDialog( () ->
testParser.loadProperties( props.stream() )
);
assertEquals( 3, testCount );
Profile testProfile = testParser.getProfile();
assertEquals( testProfile, workingProfile );
}
🟦 public void testInvalidPropertyName()
This method verifies that our code reacts correctly when encountering an invalid property name. It inserts two name/value pairs with invalid names, one before the first CLASS directive and one after. The code follows.
public void testInvalidPropertyName()
{
// Add a property with an invalid name before the fist class
// declaration, and another one after. Should yield two error
// dialogs.
List<String> props = getTestPropertyList();
props.add( 3, "notAValidPropertyName" + " " + 10 );
// After adding previous line, first class declaration is at
// line 4. Add the next invalid line at line 5.
props.add( 5, "alsoNotValid" + " " + 2 );
ProfileParser testParser = new ProfileParser();
int testCount = expectDialog( () ->
testParser.loadProperties( props.stream() )
);
assertEquals( 2, testCount );
Profile testProfile = testParser.getProfile();
assertEquals( testProfile, workingProfile );
}
🟦 public void testInvalidPropertyValue()
This method verifies that our code reacts correctly when encountering an invalid property value. It inserts one name/value pair with an invalid float value and one with an invalid color value. (We can’t test for an invalid Boolean value; every string is valid in this context.) Here’s the code for this test.
@Test
public void testInvalidPropertyValue()
{
List<String> props = getTestPropertyList();
props.add( 6, ProfileParser.LENGTH + " " + "xx" );
props.add( 7, ProfileParser.COLOR + " " + "yy" );
ProfileParser testParser = new ProfileParser();
int testCount = expectDialog( () ->
testParser.loadProperties( props.stream() )
);
assertEquals( 2, testCount );
Profile testProfile = testParser.getProfile();
assertEquals( testProfile, workingProfile );
}
🟦 public void testNVPairBeforeClassDecl()
This method verifies that our code works when a name/value pair is incorrectly placed before any CLASS directive. The code is left as an exercise for the reader. The solution can be found in the GitHub repository.
🟦 public void testMisplacedPropertyMisc()
This is a more extended version of testMisplacedProperty. I added it after the first time I generated test coverage and noticed we missed coverage on a couple of if statements. Execution requires many error dialogs to be posted, causing the test to run unusually long, so we had to raise the JUnit @Timeout value. The property list it generates is shown below. It generates the following error messages:
- Lines 8-12: These lines are in error because the properties declared are not members of the GraphPropertySetMW class declared on line 7.
- Lines 14-20: These lines are in error because the properties declared are not members of the LinePropertySetAxes class declared on line 13.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | PROFILE default_MUTATE gridUnit 66.0 CLASS GraphPropertySetMW font_size 20.0 CLASS LinePropertySetAxes stroke 12.0 CLASS GraphPropertySetMW stroke 2 length 2 spacing 2 draw 2 color 2 CLASS LinePropertySetAxes font_bold false font_italic 2 font_name 2 font_draw 2 bgColor 2 fgColor 2 width 2 |
The code for testMisplacedPropertyMisc looks like this.
@Timeout( 5 )
@Test
public void testMisplacedPropertyMisc()
{
List<String> props = getTestPropertyList();
String lineName =
LinePropertySetAxes.class.getSimpleName();
String graphName =
GraphPropertySetMW.class.getSimpleName();
props.add( ProfileParser.CLASS + " " + graphName );
props.add( ProfileParser.STROKE + " " + 2 );
props.add( ProfileParser.LENGTH + " " + 2 );
props.add( ProfileParser.SPACING + " " + 2 );
props.add( ProfileParser.DRAW + " " + 2 );
props.add( ProfileParser.COLOR + " " + 2 );
props.add( ProfileParser.CLASS + " " + lineName );
props.add( ProfileParser.FONT_BOLD + " " + false );
props.add( ProfileParser.FONT_ITALIC + " " + 2 );
props.add( ProfileParser.FONT_NAME + " " + 2 );
props.add( ProfileParser.FONT_DRAW + " " + 2 );
props.add( ProfileParser.BG_COLOR + " " + 2 );
props.add( ProfileParser.FG_COLOR + " " + 2 );
props.add( ProfileParser.WIDTH + " " + 2 );
ProfileParser testParser = new ProfileParser();
int testCount = expectDialog( () ->
testParser.loadProperties( props.stream() )
);
assertEquals( 12, testCount );
Profile testProfile = testParser.getProfile();
assertEquals( testProfile, workingProfile );
}
Summary
On this page, we implemented a unit test for the ProfileParser class. You may have noticed that most of our code was related to validating input processing; this is not unusual, particularly when the input potentially comes from a human. On the next page, we’ll look at a facility that uses the ProfileParser to store Profile data in a file and read it back on request.