JUnit 5 - Convert multiple CSV file parameters into single object
JUnit 5 - Convert multiple CSV file parameters into single object
Hello there Java testers.
I started working with JUnit 5 a few days ago because I loved the new way of creating parameterized tests.
There's the @ParameterizedTest that allows a test to run in a parameterized fashion and @CsvFileSource that loads the parameters from a CSV file.
@ParameterizedTest
@CsvFileSource
The thing is that I have too much columns in my CSV and don't want to have a huge method signature in my unit test. Let me give you an example:
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv")
void myTest(String p1, String p2, String p3, String p4, String p5, String p6) {
//test using parameters
}
I'd like to know if there's some kind of converter that can do something like this for me:
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv")
void myTest(@ConvertWith(TestDataConverter.class) TestData testData) {
//test using parameters
}
static class TestDataConverter implements TestConverter{
public TestData convert(Object ... params){
//a simple method that creates the TestData object and inserts the params in it
}
}
And that's it.
Interesting idea. Can you please open a feature request over at github.com/junit-team/junit5/issues
– Sormuras
Nov 29 '17 at 12:27
FYI: I updated my answer to cover the argument aggregation support introduce in JUnit Jupiter 5.2. You should really check that out! ;-)
– Sam Brannen
Jun 30 at 13:57
3 Answers
3
Updated Answer
As of JUnit Jupiter 5.2: Yes, there are dedicated ArgumentsAggregator and ArgumentsAccessor APIs exactly for this purpose.
ArgumentsAggregator
ArgumentsAccessor
Take a look at the PersonAggregator example in the JUnit User Guide for a concrete example.
PersonAggregator
Original Answer
As of JUnit Jupiter 5.0: No, there is currently no out-of-the-box converter that could do that for you.
Reason: The parameterized test support in JUnit Jupiter does not support mapping from multiple arguments to a single argument. So, as @Sormuras suggested, you could open an issue to recommend that.
In terms of performing the actual conversion, you could consider using uniVocity-parsers, which JUnit Jupiter uses internally to parse CSV files. uniVocity-parsers also has support for mapping directly from CSV files onto beans, but to use that you'd need to implement your own @TestTemplate that reads the CSV files and performs the mappings.
@TestTemplate
Other options include the CSV support from the Jackson framework or from the JSefa project.
Thanks for sharing the knowledge Sam. I'll open an issue for that feature. In the meantime I'll just have a giant method signature. The cost of creating my own
@TestTemplate is not worth it.– de.la.ru
Nov 29 '17 at 14:56
@TestTemplate
Sounds good, and thanks for creating the issue on GitHub.
– Sam Brannen
Nov 29 '17 at 16:40
Not a general solution, but one little trick that sometimes could work:
sed
awk
now implement the TestConverter in order to transform the single string into a String to be used as you prefer. In many cases it could be as simple as:
static class TestDataConverter implements TestConverter{
public TestData convert(String string){
String params = string.split(",");
//a simple method that creates the TestData object and inserts the params in it
}
}
A very common case that does not permit a direct use of the trick is when quotes are already used in the CSV in order to deal with 'comma' inside a field:
Name, complex field, City
Carlo, "this field could contain , and other critical chars" , London
Mario, "this field could contain , and other critical chars" , New York
Luca, "this field could contain , and other critical chars" , Milan
Quoting lines with the same quotes clearly does not work.
This or similar cases could however be solved with some additional (but boring) transformation of the CSV file.
For example assuming that 3 consecutive sharps "###” can be considered as a consistent separator:
"Name###complex field###City"
"Carlo###this field could contain , and other critical chars###London"
"Mario###this field could contain , and other critical chars###New York"
"Luca###this field could contain , and other critical chars###Milan"
I knew... it is a trick.. not a definitive and elegant solution... :-D
clearly in this case the split will be on the 3 hashes:
String params = string.split("###");
Yeah that would definitely work. I could have a helper class with a method like
TestHelper,parseCsvData(csvData) that could return a CsvTestData POJO. One problem is that I have quotes in the data already so it could get kinda messy, would have to escape it.– de.la.ru
Nov 29 '17 at 17:06
TestHelper,parseCsvData(csvData)
CsvTestData
quotes in the CSV are exactly the kind of problems that I had in mind that make the trick not directly and generally usable. However with some more transformation in the CSV it could be adapted. It is possible to skip quotes inside the CSV by avoiding ambiguities with the use of a separator (for example a multichar separator '%£$' instead of the simple comma) that cannot appear inside a field. I add a note to the answer.
– Carlo Bellettini
Nov 29 '17 at 17:25
You can use this Aggregator, albeit somewhat hacky:
public class CsvToMapAggregator implements ArgumentsAggregator {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(CsvToMapAggregator.class)
public @interface CsvToMap {}
private static String keys = null; // ATTN: Do not use in parallel runs !!!
@Override public Map<String, Object> aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
Object values = accessor.toArray();
if (keys == null) {
keys = (String) values; // First Line: Header
} else if (values.length != keys.length) {
keys = null; // Last Line: End of Data
return null;
}
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < values.length; i++) {
map.put(keys[i], values[i]); // Read Data from Line
}
return map;
}
}
Note:
All lines must have the same number of items.
The csv file must end with an additional line (e.g."EOF" in first column) that differs in that number from the previous lines.
The test method will look like this, to skip the header and end line:
@ParameterizedTest(name = "{0}")
@CsvFileSource(resources = "/testcases.csv", delimiter = ';')
@DisplayName("read complete .csv and map columns")
void testCsvData(@CsvToMap Map<String, Object> map, TestInfo info) {
switch (info.getDisplayName()) {
case "Testcase": // First Line: Header
case "EOF": // Last Line: End of File
break;
default:
assertNotNull(map);
assertFalse(map.isEmpty());
}
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
I think, this somehow boils down to ... asking us to recommend or find a book, tool, software library, tutorial or other off-site resource ... which is off-topic for Stack Overflow as its tend to attract opinionated answers and spam. You might try your luck at softwarereqs though.
– GhostCat
Nov 29 '17 at 12:23