Junit5, @CsvSource and escaping
JUnit 5 API brings as a first citizen templatized tests. Compared to JUnit 4 it has multiple flavors to match simple, powerful and advanced cases as smoothly as possible. Part of that revamping is the @CsvSource API. However this simplification brings for the intermediate case a new challenge: the escaping!
To illustrate it, I will use a case I met recently: JSON data type. High level I want to write a parameterized test where I provide to the test method the input (JSON), the result state (true to mean it was matched, false otherwise) and the expected outputs in case of matching.
Here is the table of the scenarii I want to test:
Input | Matching result | Type output | Data output |
{"type": "STATE", "value": {}} | true | STATE | {} |
{"type": "RECORD", "record": {"name":"foo"}} | true | RECORD | ["name":"foo"} |
dddesdd | false | - | - |
The test can therefore be written like that in JUnit 5:
@ParameterizedTest
@CsvSource({
// matching
"{\"type\": \"STATE\", \"value\": {}},true,STATE,{}",
"{\"type\": \"RECORD\", \"record\": {\"name\": \"foo\"}},true,RECORD,{\"name\":\"foo\"}",
// no matching
"dddesdd,false,,,,"
})
void matches(final String line, final boolean isMatched,
final String type, final String data) throws IOException {
final Optional<Matching> matching = doMatches(line); // tested impl
assertEquals(isMatched, matching.isPresent());
if (!isMatched) {
return; // nothing to match since it didn't match
}
final Matching result = matching.orElseThrow(IllegalArgumentException::new);
assertEquals(type, result.getType());
assertEquals(data, result.getData());
}
This implementation is a 1-1 from the previous table mapped to the JUnit 5 API. However it does not work. The reason is as simple as the JSON contains "," which is the CSV separator. A trivial way to solve it is to move to a @MethodSource and return exactly the structure you want but it breaks a bit the binding of the test and data since you will need two methods for the same test. Another solution which is more relevant for this case is to switch the delimit to something not contained in the test data. Here we can use the "|" for instance:
@CsvSource(value = {
// matching
"{\"type\": \"STATE\", \"value\": {}}|true|STATE|{}",
"{\"type\": \"RECORD\", \"record\": {\"name\": \"foo\"}}|true|RECORD|{\"name\":\"foo\"}",
// no matching
"dddesdd|false||||"
}, delimiter = '|')
This simple modification will enable the CSV parser of JUnit 5 (univocity one which is shaded in JUnit 5 artifacts) to parse properly our strings and inject the right values in our test method.
You can use the same trick for string literals (quoted strings), the escape character is "'" - simple quote - but this one is currently hardcoded so don't try to change it. You can wonder why previous example works if the quote escape character is "'" and my JSON only contains java string escaping and not "'\""? This is because the default (hardcoded here again) CSV string character is "'" as well. In other words you can use that:
'This is a string with, separator','this is ''another'' one',this one is,split
To actually map this data table:
This is a string with, separator | this is 'another' one | this one is | split |
Here we are, we managed to put JSON in a @CsvSource and saw how to inject almost any t-uples primitives through a parameterized test of JUnit 5. This really enables to concentrate on test data, limiting a lot the boilerplate to write to create such tests which is key to have a good quality. It is then trivial to enrich that with production cases and debug them very simply and quickly.
From the same author:
In the same category: