Published on

Using JUnit 5's CSV Data Source

Authors

As you may have gathered from previous posts I am a big fan of JUnit 5's parameterized tests. I really like the way you can write one test which can be given different parameters for each test run and thereby give you very good test coverage without having an explosion of different test methods.

Today I wanted to augment the tests we already had in our code. A bunch of tests were manually run against the application in our test environment and were signed off. I gathered the inputs and outputs for this as a CSV. JUnit 5 does have this covered using @CsvFileSource if you want to feed your test a CSV file you have saved or a @CsvSource if you want to define the CSV on top of the test. The file used had multiple columns as input and a few columns as the expected result for each test. One thing to note is if you want to you can pass the @CsvFileSource annotation multiple CSV files where you may want to for example use one CSV as the data that feeds into your test and the other as the expected results for each run.

In my case I had a CSV with multiple input columns and a few expected result columns similar to below dummy example (note the mix of data types like string, BigDecimal, LocalDate and enum):

name,surname,dateOfBirth,salary,expectedRanking,expectedTag
John,Smith,1994-08-16,100000,9.7513,SILVER
Sally,Jones,1990-04-10,200000,3.3631,GOLD
...

As I was using a file the @CsvFileSource was obviously what I wanted to use. Looking at the example you can either have one method parameter per column in the CSV in your test method or write an argument converter. I initially tried to go the argument converter route but discovered that the convert method currently only passes one cell at a time to the convert method which for a multi-column CSV is not suitable at all. There is an issue raised for it over here but as of the writing of this post has not been resolved yet. So I went the other route as below:

@ParameterizedTest
@CsvFileSource(resources = arrayOf("myCSV.csv"), numLinesToSkip = 1)
fun `codeToTest with valid inputs calculates the expected rank and tag correctly`(name: String,
           surname: String,
           dateOfBirth: LocalDate,
           salary: BigDecimal,
           expectedRanking: BigDecimal,
           expectedTag: TagEnum) {

    val (actualRanking, actualTag) = codeToTest(name, surname, dateOfBirth, salary)
    ...
    assertThat(actualRanking, `is`(closeTo(expectedRanking, BigDecimal.ZERO)))
    assertThat(actualTag, `is`(equalTo(expectedTag)))
}

In the above code I read the example CSV file from earlier. I tell JUnit to skip the first line as that is the header row. It then automatically converts the values in the file to my method parameters. It infers the type based on the test method parameter types. I have only checked this against the types in my method parameter list but it should be able to convert to many other common types out of the box. An easy way to check it is behaving as expected is before writing the actual test simply output the parameters to see it is converting as expected something like below:

@ParameterizedTest
@CsvFileSource(resources = arrayOf("myCSV.csv"), numLinesToSkip = 1)
fun `codeToTest with valid inputs calculates the expected rank and tag correctly`(name: String,
           surname: String,
           dateOfBirth: LocalDate,
           salary: BigDecimal,
           expectedRanking: BigDecimal,
           expectedTag: TagEnum) {

        println("name: [$name], surname: [$surname], dateOfBirth: [$dateOfBirth], salary: [$salary], expectedRanking: [$expectedRanking], expectedTag: [$expectedTag]")
}