One of the most underrated Maven configurations: maven.config
February 27, 2024How to simulate real BeforeAll and AfterAll in JUnit 5
July 3, 2024Introduction
This article belongs to the Managing Test Data series.
I will show you different ways to smartly use testing framework features when the date changes but the test output is the same.
In this article, you will see one use case for the @CsvSource
and @CsvFileSource
features. These annotations are particularly useful when you want to provide test data in CSV (Comma-Separated Values) format either directly in the source code or from an external file.
@CsvSource
This Source of Argument in JUnit 5 allows you to use the data as CSV values to allow you either a fast prototype of a CSV file consumption in a test or by the simple use of it to give you the advantage of readability for small data sets. It has different ways to configure it, but let’s focus first on the basic usage.
class CsvSourceTest {
private static final String MAXIMUM_PRICE = "30.0";
@DisplayName("Products should not exceed the maximum price")
@ParameterizedTest(name = "product ''{0}'' of amount ${1} does not exceeds $" + MAXIMUM_PRICE)
@CsvSource({
"Micro SD Card 16Gb, 6.09",
"JBL GO 2, 22.37",
"iPad Air Case, 14.99",
})
void productsLassThan(String product, BigDecimal amount) {
assertThat(product).isNotEmpty();
assertThat(amount).isLessThanOrEqualTo(new BigDecimal(MAXIMUM_PRICE));
}
}
Lines 6 to 11 show you the basic usage of the @CsvSource
. Note that:
- The CSV data is placed between brackets in the annotation
- Each CSV line has double quotes at the beginning and end of it
- All the values are separated by a comma, after the end of the double quotes.
The test output will look like this:
CsvSourceTest
└─ Products should not exceed the maximum price
├─ [1] product 'Micro SD Card 16Gb' of amount $6.09 does not exceeds $30.0 ✔
├─ [2] product 'JBL GO 2' of amount $22.37 does not exceeds $30.0 ✔
└─ [3] product 'iPad Air Case' of amount $14.99 does not exceeds $30.0 ✔
Annotation configurations
There are different ways to change the @CsvSource
behavior:
Annotation configuration | What it does |
value | This is the default. |
delimiter | Set the delimiter as a char. The default value is a comma ( , ) |
delimiterString | Set the delimiter as a String .The default is a comma ( , ) |
emptyValue | Set what will be added as an empty value. The default is an empty value set as a double quote ( "" )The empty value should be added as a single quote on the line value. |
ignoreLeadingAndTrailingWhitespace | The leading and trailing whitespace is automatic tribber by default. Set it to true to ignore it. |
maxCharsPerColum | Sets the max chars in the column. The default is 4096. |
nullValues | Add the ability to set a specific value to the CSv to null. |
quoteChar | Determine which character will be used to quote the value. You quote the value when you need to add either a space or any char that might fail the process of reading the line. The default is single quotes ( '' ). |
textBlock | Allows you to use the Text Blocks feature of Java >= 15. It also gives the ability to add comments in the data source which must begin with # |
useHeadersInDisplayName | Set to true when you want to use a header in the first line. This option is set by default as false . |
When you use a configuration the value
attribute must be explicitly declared. You see the example in the below extra explanation.
nullValues
Use it when you want to make a specific value to null.
@CsvSource(nullValues = "6.09", value = {
"Micro SD Card 16Gb, 6.09",
"JBL GO 2, 22.37",
"iPad Air Case, 14.99",
})
In the code snippet above you can see that the nullValues
is using "6.09"
. The first value the @CsvSource
contains this value, so it will be replaced by a null value instead of using the original one.
textBlock
It allows you to use the Text Blocks feature from Java >= 15, so you don’t need to wrap up the value in the curly brackets. Instead, use the """
as you can see in the example below.
@CsvSource(textBlock = """
"Micro SD Card 16Gb, 6.09",
"JBL GO 2, 22.37",
"iPad Air Case, 14.99"
"""
)
Why using @CsvSource?
Inline CSV data will make the test method more readable and concise, especially when you are dealing with a small number of test cases, so you get the benefit of readability and conciseness.
All test data is directly visible within the test method, making it easy for developers to understand the different scenarios being tested without having to navigate to external files adding immediate visibility, and reducing external dependencies since the test data is embedded within the source code, you don’t need external CSV files, which can simplify the project structure and reduce dependencies on external resources.
It’s easy to maintain for a small set of test data, helping you to prototype possible scenarios using a CSV file without adding all the dependencies, external files, and complexity.
@CsvFileSource
The @CsvFileSource
allows you to provide test data from an external CSV file. This is especially useful when you have a larger set of test data that you want to manage separately from your test code.
class CsvFileSourceTest {
private static final String MAXIMUM_PRICE = "30.0";
@DisplayName("Products should not exceed the maximum price")
@ParameterizedTest(name = "product ''{0}'' of amount ${1} does not exceeds $" + MAXIMUM_PRICE)
@CsvFileSource(resources = "/products.csv")
void productsLassThan(String product, BigDecimal amount) {
assertThat(product).isNotEmpty();
assertThat(amount).isLessThanOrEqualTo(new BigDecimal(MAXIMUM_PRICE));
}
}
Line 6 shows the usage of the @CsvFileSource
. Not that is using the attribute resources
, which will look at the file provided in the resource folders. The default behavior is also to expect the CSV file without a header. You can see a possible file for this test execution:
Micro SD Card 16Gb,6.09
JBL GO 2,22.37
iPad Air Case, 14.99
The particularities
These are the following particularities compared to the @CsvSource
.
Same configuration as the @CsvSource
The @CsvFileSource
has the same annotation configuration as the @CsvSource
, except for the change in the expected quoteChar as it expects double quotes (""
) instead of single ones.
Extra configurations
Annotation configuration | What it does |
files | Read the external file as a file, so it requires a full path based on the project classpath. |
numLinesToSkip | Skip N lines as N is the number of lines to skip. This is useful when you have a header in the file, where you can set it as 1 .The default value is 0 . |
encoding | Changes the file encoding. The default value is UTF-8 . |
resources and files
I believe you noticed that both are in the plural. This means that you can set multiple files. Only single quotes are necessary when you add only one file, as seen in the previous example.
In case, you want to merge multiple files, simply add the files separated by commas wrapped up in curly brackets.
@DisplayName("Products should not exceed the maximum price")
@ParameterizedTest(name = "product ''{0}'' of amount ${1} does not exceeds $" + MAXIMUM_PRICE)
@CsvFileSource(resources = {"/products.csv", "/products-no.csv"}, numLinesToSkip = 1)
void productsLassThan(String product, BigDecimal amount) {
assertThat(product).isNotEmpty();
assertThat(amount).isLessThanOrEqualTo(new BigDecimal(MAXIMUM_PRICE));
}
Line 3 shows the usage of two files: products.csv
and products-no.csv
.
They must share the same columns you are using in your parameter conversion, the ones you add in your test like String product
and BigDecimal amount
in this example.
If you use a column and parameter that is present in one file, but not in the other, a ParameterResolutionException
will be thrown.
When to use these annotations?
The @CsvFileSource
is straightforward: use it when you need to use an external CSV file in your tests.
The author recommends you use the @CsvSource
while protecting your data-driven tests using only primitive values or object-related ones like String, Integer, and so on. Otherwise, use any other Source of Arguments provided by JUnit 5.