Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4ef6e70

Browse files
committed
Support CSV headers in display names in parameterized tests
Given the following parameterized test that sets useHeadersInDisplayName to true and uses {arguments} instead of {argumentsWithNames} for its display name pattern... @ParameterizedTest(name = "[{index}] {arguments}") @CsvSource(useHeadersInDisplayName = true, textBlock = """ FRUIT, RANK apple, 1 banana, 2 cherry, 3 """) void test(String fruit, int rank) {} The generated display names are: [1] FRUIT = apple, RANK = 1 [2] FRUIT = banana, RANK = 2 [3] FRUIT = cherry, RANK = 3 See #2759
1 parent 69aed70 commit 4ef6e70

File tree

14 files changed

+345
-84
lines changed

14 files changed

+345
-84
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.8.2.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*Scope:*
77

88
* Text blocks in `@CsvSource` are treated like CSV files
9+
* CSV headers in display names for `@CsvSource` and `@CsvFileSource`
910
* Custom quote character support in `@CsvSource` and `@CsvFileSource`
1011

1112
For a complete list of all _closed_ issues and pull requests for this release, consult the
@@ -29,8 +30,13 @@ No changes.
2930
quoted strings. See the
3031
<<../user-guide/index.adoc#writing-tests-parameterized-tests-sources-CsvSource, User
3132
Guide>> for details and examples.
33+
* CSV headers can now be used in display names in parameterized tests. See
34+
<<../user-guide/index.adoc#writing-tests-parameterized-tests-sources-CsvSource,
35+
`@CsvSource`>> and
36+
<<../user-guide/index.adoc#writing-tests-parameterized-tests-sources-CsvFileSource,
37+
`@CsvFileSource`>> in the User Guide for details and examples.
3238
* The quote character for _quoted strings_ in `@CsvSource` and `@CsvFileSource` is now
33-
configurable via new `quoteCharacter` attributes in each annotation.
39+
configurable via a new `quoteCharacter` attribute in each annotation.
3440

3541

3642
[[release-notes-5.8.2-junit-vintage]]

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,7 +1332,9 @@ include::{testDir}/example/ExternalMethodSourceDemo.java[tags=external_MethodSou
13321332

13331333
`@CsvSource` allows you to express argument lists as comma-separated values (i.e., CSV
13341334
`String` literals). Each string provided via the `value` attribute in `@CsvSource`
1335-
represents a CSV record and results in one invocation of the parameterized test.
1335+
represents a CSV record and results in one invocation of the parameterized test. The first
1336+
record may optionally be used to supply CSV headers (see the Javadoc for the
1337+
`useHeadersInDisplayName` attribute for details and an example).
13361338

13371339
[source,java,indent=0]
13381340
----
@@ -1375,12 +1377,16 @@ by default. This behavior can be changed by setting the
13751377
If the programming language you are using supports _text blocks_ -- for example, Java SE
13761378
15 or higher -- you can alternatively use the `textBlock` attribute of `@CsvSource`. Each
13771379
record within a text block represents a CSV record and results in one invocation of the
1378-
parameterized test. Using a text block, the previous example can be implemented as follows.
1380+
parameterized test. The first record may optionally be used to supply CSV headers by
1381+
setting the `useHeadersInDisplayName` attribute to `true` as in the example below.
1382+
1383+
Using a text block, the previous example can be implemented as follows.
13791384

13801385
[source,java,indent=0]
13811386
----
1382-
@ParameterizedTest
1383-
@CsvSource(textBlock = """
1387+
@ParameterizedTest(name = "[{index}] {arguments}")
1388+
@CsvSource(useHeadersInDisplayName = true, textBlock = """
1389+
FRUIT, RANK
13841390
apple, 1
13851391
banana, 2
13861392
'lemon, lime', 0xF1
@@ -1391,6 +1397,15 @@ void testWithCsvSource(String fruit, int rank) {
13911397
}
13921398
----
13931399

1400+
The generated display names for the previous example include the CSV header names.
1401+
1402+
----
1403+
[1] FRUIT = apple, RANK = 1
1404+
[2] FRUIT = banana, RANK = 2
1405+
[3] FRUIT = lemon, lime, RANK = 0xF1
1406+
[4] FRUIT = strawberry, RANK = 700_000
1407+
----
1408+
13941409
In contrast to CSV records supplied via the `value` attribute, a text block can contain
13951410
comments. Any line beginning with a `+++#+++` symbol will be treated as a comment and
13961411
ignored. Note, however, that the `+++#+++` symbol must be the first character on the line
@@ -1435,7 +1450,11 @@ your text block.
14351450

14361451
`@CsvFileSource` lets you use comma-separated value (CSV) files from the classpath or the
14371452
local file system. Each record from a CSV file results in one invocation of the
1438-
parameterized test.
1453+
parameterized test. The first record may optionally be used to supply CSV headers. You can
1454+
instruct JUnit to ignore the headers via the `numLinesToSkip` attribute. If you would like
1455+
for the headers to be used in the display names, you can set the `useHeadersInDisplayName`
1456+
attribute to `true`. The examples below demonstrate the use of `numLinesToSkip` and
1457+
`useHeadersInDisplayName`.
14391458

14401459
The default delimiter is a comma (`,`), but you can use another character by setting the
14411460
`delimiter` attribute. Alternatively, the `delimiterString` attribute allows you to use a
@@ -1457,6 +1476,26 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=CsvFileSource_example
14571476
include::{testResourcesDir}/two-column.csv[]
14581477
----
14591478

1479+
The following listing shows the generated display names for the first two parameterized
1480+
test methods above.
1481+
1482+
----
1483+
[1] country=Sweden, reference=1
1484+
[2] country=Poland, reference=2
1485+
[3] country=United States of America, reference=3
1486+
[4] country=France, reference=700_000
1487+
----
1488+
1489+
The following listing shows the generated display names for the last parameterized test
1490+
method above that uses CSV header names.
1491+
1492+
----
1493+
[1] COUNTRY = Sweden, REFERENCE = 1
1494+
[2] COUNTRY = Poland, REFERENCE = 2
1495+
[3] COUNTRY = United States of America, REFERENCE = 3
1496+
[4] COUNTRY = France, REFERENCE = 700_000
1497+
----
1498+
14601499
In contrast to the default syntax used in `@CsvSource`, `@CsvFileSource` uses a double
14611500
quote (`+++"+++`) as the quote character by default, but this can be changed via the
14621501
`quoteCharacter` attribute. See the `"United States of America"` value in the example

documentation/src/test/java/example/ParameterizedTestDemo.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,13 @@ void testWithCsvFileSourceFromFile(String country, int reference) {
236236
assertNotNull(country);
237237
assertNotEquals(0, reference);
238238
}
239+
240+
@ParameterizedTest(name = "[{index}] {arguments}")
241+
@CsvFileSource(resources = "/two-column.csv", useHeadersInDisplayName = true)
242+
void testWithCsvFileSourceAndHeaders(String country, int reference) {
243+
assertNotNull(country);
244+
assertNotEquals(0, reference);
245+
}
239246
// end::CsvFileSource_example[]
240247

241248
// tag::ArgumentsSource_example[]

documentation/src/test/resources/two-column.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Country, Reference
1+
COUNTRY, REFERENCE
22
Sweden, 1
33
Poland, 2
44
"United States of America", 3

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import java.io.StringReader;
1717
import java.lang.annotation.Annotation;
18+
import java.util.ArrayList;
1819
import java.util.Arrays;
1920
import java.util.List;
2021
import java.util.Set;
@@ -23,6 +24,7 @@
2324

2425
import com.univocity.parsers.csv.CsvParser;
2526

27+
import org.junit.jupiter.api.Named;
2628
import org.junit.jupiter.api.extension.ExtensionContext;
2729
import org.junit.jupiter.params.support.AnnotationConsumer;
2830
import org.junit.platform.commons.PreconditionViolationException;
@@ -53,56 +55,96 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
5355
Preconditions.condition(this.annotation.value().length > 0 ^ textBlockDeclared,
5456
() -> "@CsvSource must be declared with either `value` or `textBlock` but not both");
5557

56-
if (textBlockDeclared) {
57-
return parseTextBlock(this.annotation.textBlock()).stream().map(Arguments::of);
58-
}
59-
60-
AtomicInteger index = new AtomicInteger(0);
61-
// @formatter:off
62-
return Arrays.stream(this.annotation.value())
63-
.map(line -> parseLine(line, index.incrementAndGet()))
64-
.map(Arguments::of);
65-
// @formatter:on
58+
return textBlockDeclared ? parseTextBlock() : parseValueArray();
6659
}
6760

68-
private List<String[]> parseTextBlock(String textBlock) {
61+
private Stream<Arguments> parseTextBlock() {
62+
String textBlock = this.annotation.textBlock();
63+
boolean useHeadersInDisplayName = this.annotation.useHeadersInDisplayName();
64+
List<Arguments> argumentsList = new ArrayList<>();
65+
6966
try {
70-
AtomicInteger index = new AtomicInteger(0);
7167
List<String[]> csvRecords = this.csvParser.parseAll(new StringReader(textBlock));
68+
String[] headers = useHeadersInDisplayName ? getHeaders(this.csvParser) : null;
69+
70+
AtomicInteger index = new AtomicInteger(0);
7271
for (String[] csvRecord : csvRecords) {
7372
index.incrementAndGet();
7473
Preconditions.notNull(csvRecord,
75-
() -> "Line at index " + index.get() + " contains invalid CSV: \"\"\"\n" + textBlock + "\n\"\"\"");
76-
processNullValues(csvRecord, this.nullValues);
74+
() -> "Record at index " + index + " contains invalid CSV: \"\"\"\n" + textBlock + "\n\"\"\"");
75+
argumentsList.add(processCsvRecord(csvRecord, this.nullValues, useHeadersInDisplayName, headers));
7776
}
78-
return csvRecords;
7977
}
8078
catch (Throwable throwable) {
8179
throw handleCsvException(throwable, this.annotation);
8280
}
81+
82+
return argumentsList.stream();
8383
}
8484

85-
private String[] parseLine(String line, int index) {
85+
private Stream<Arguments> parseValueArray() {
86+
boolean useHeadersInDisplayName = this.annotation.useHeadersInDisplayName();
87+
List<Arguments> argumentsList = new ArrayList<>();
88+
8689
try {
87-
String[] csvRecord = this.csvParser.parseLine(line + LINE_SEPARATOR);
88-
Preconditions.notNull(csvRecord,
89-
() -> "Line at index " + index + " contains invalid CSV: \"" + line + "\"");
90-
processNullValues(csvRecord, this.nullValues);
91-
return csvRecord;
90+
String[] headers = null;
91+
AtomicInteger index = new AtomicInteger(0);
92+
for (String input : this.annotation.value()) {
93+
index.incrementAndGet();
94+
String[] csvRecord = this.csvParser.parseLine(input + LINE_SEPARATOR);
95+
// Lazily retrieve headers if necessary.
96+
if (useHeadersInDisplayName && headers == null) {
97+
headers = getHeaders(this.csvParser);
98+
}
99+
Preconditions.notNull(csvRecord,
100+
() -> "Record at index " + index + " contains invalid CSV: \"" + input + "\"");
101+
argumentsList.add(processCsvRecord(csvRecord, this.nullValues, useHeadersInDisplayName, headers));
102+
}
92103
}
93104
catch (Throwable throwable) {
94105
throw handleCsvException(throwable, this.annotation);
95106
}
107+
108+
return argumentsList.stream();
96109
}
97110

98-
static void processNullValues(String[] csvRecord, Set<String> nullValues) {
99-
if (!nullValues.isEmpty()) {
100-
for (int i = 0; i < csvRecord.length; i++) {
101-
if (nullValues.contains(csvRecord[i])) {
102-
csvRecord[i] = null;
103-
}
111+
// Cannot get parsed headers until after parsing has started.
112+
static String[] getHeaders(CsvParser csvParser) {
113+
return Arrays.stream(csvParser.getContext().parsedHeaders())//
114+
.map(String::trim)//
115+
.toArray(String[]::new);
116+
}
117+
118+
/**
119+
* Processes custom null values, supports wrapping of column values in
120+
* {@link Named} if necessary (for CSV header support), and returns the
121+
* CSV record wrapped in an {@link Arguments} instance.
122+
*/
123+
static Arguments processCsvRecord(Object[] csvRecord, Set<String> nullValues, boolean useHeadersInDisplayName,
124+
String[] headers) {
125+
126+
// Nothing to process?
127+
if (nullValues.isEmpty() && !useHeadersInDisplayName) {
128+
return Arguments.of(csvRecord);
129+
}
130+
131+
Preconditions.condition(!useHeadersInDisplayName || (csvRecord.length <= headers.length),
132+
() -> String.format(
133+
"The number of columns (%d) exceeds the number of supplied headers (%d) in CSV record: %s",
134+
csvRecord.length, headers.length, Arrays.toString(csvRecord)));
135+
136+
Object[] arguments = new Object[csvRecord.length];
137+
for (int i = 0; i < csvRecord.length; i++) {
138+
Object column = csvRecord[i];
139+
if (nullValues.contains(column)) {
140+
column = null;
141+
}
142+
if (useHeadersInDisplayName) {
143+
column = Named.of(headers[i] + " = " + column, column);
104144
}
145+
arguments[i] = column;
105146
}
147+
return Arguments.of(arguments);
106148
}
107149

108150
/**

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import static java.util.Spliterators.spliteratorUnknownSize;
1414
import static java.util.stream.Collectors.toList;
1515
import static java.util.stream.StreamSupport.stream;
16-
import static org.junit.jupiter.params.provider.Arguments.arguments;
16+
import static org.junit.jupiter.params.provider.CsvArgumentsProvider.getHeaders;
1717
import static org.junit.jupiter.params.provider.CsvArgumentsProvider.handleCsvException;
18-
import static org.junit.jupiter.params.provider.CsvArgumentsProvider.processNullValues;
18+
import static org.junit.jupiter.params.provider.CsvArgumentsProvider.processCsvRecord;
1919
import static org.junit.jupiter.params.provider.CsvParserFactory.createParserFor;
2020
import static org.junit.platform.commons.util.CollectionUtils.toSet;
2121

@@ -119,41 +119,49 @@ private static class CsvParserIterator implements Iterator<Arguments> {
119119

120120
private final CsvParser csvParser;
121121
private final CsvFileSource annotation;
122+
private final boolean useHeadersInDisplayName;
122123
private final Set<String> nullValues;
123-
private Object[] nextCsvRecord;
124+
private Arguments nextArguments;
125+
private String[] headers;
124126

125127
CsvParserIterator(CsvParser csvParser, CsvFileSource annotation) {
126128
this.csvParser = csvParser;
127129
this.annotation = annotation;
130+
this.useHeadersInDisplayName = annotation.useHeadersInDisplayName();
128131
this.nullValues = toSet(annotation.nullValues());
129132
advance();
130133
}
131134

132135
@Override
133136
public boolean hasNext() {
134-
return this.nextCsvRecord != null;
137+
return this.nextArguments != null;
135138
}
136139

137140
@Override
138141
public Arguments next() {
139-
Arguments result = arguments(this.nextCsvRecord);
142+
Arguments result = this.nextArguments;
140143
advance();
141144
return result;
142145
}
143146

144147
private void advance() {
145-
String[] csvRecord = null;
146148
try {
147-
csvRecord = this.csvParser.parseNext();
149+
String[] csvRecord = this.csvParser.parseNext();
148150
if (csvRecord != null) {
149-
processNullValues(csvRecord, this.nullValues);
151+
// Lazily retrieve headers if necessary.
152+
if (this.useHeadersInDisplayName && this.headers == null) {
153+
this.headers = getHeaders(this.csvParser);
154+
}
155+
this.nextArguments = processCsvRecord(csvRecord, this.nullValues, this.useHeadersInDisplayName,
156+
this.headers);
157+
}
158+
else {
159+
this.nextArguments = null;
150160
}
151161
}
152162
catch (Throwable throwable) {
153163
handleCsvException(throwable, this.annotation);
154164
}
155-
156-
this.nextCsvRecord = csvRecord;
157165
}
158166

159167
}

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
* or {@link #files}.
2828
*
2929
* <p>The CSV records parsed from these resources and files will be provided as
30-
* arguments to the annotated {@code @ParameterizedTest} method.
30+
* arguments to the annotated {@code @ParameterizedTest} method. Note that the
31+
* first record may optionally be used to supply CSV headers (see
32+
* {@link #useHeadersInDisplayName}).
3133
*
3234
* <p>Any line beginning with a {@code #} symbol will be interpreted as a comment
3335
* and will be ignored.
@@ -95,6 +97,34 @@
9597
*/
9698
String lineSeparator() default "\n";
9799

100+
/**
101+
* Configures whether the first CSV record should be treated as header names
102+
* for columns.
103+
*
104+
* <p>When set to {@code true}, the header names will be used in the
105+
* generated display name for each {@code @ParameterizedTest} method
106+
* invocation. When using this feature, you must ensure that the display name
107+
* pattern for {@code @ParameterizedTest} includes
108+
* {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_PLACEHOLDER} instead of
109+
* {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_WITH_NAMES_PLACEHOLDER}
110+
* as demonstrated in the example below.
111+
*
112+
* <p>Defaults to {@code false}.
113+
*
114+
*
115+
* <h4>Example</h4>
116+
* <pre class="code">
117+
* {@literal @}ParameterizedTest(name = "[{index}] {arguments}")
118+
* {@literal @}CsvFileSource(resources = "fruits.csv", useHeadersInDisplayName = true)
119+
* void test(String fruit, int rank) {
120+
* // ...
121+
* }</pre>
122+
*
123+
* @since 5.8.2
124+
*/
125+
@API(status = EXPERIMENTAL, since = "5.8.2")
126+
boolean useHeadersInDisplayName() default false;
127+
98128
/**
99129
* The quote character to use for <em>quoted strings</em>.
100130
*

0 commit comments

Comments
 (0)