Skip to content

Commit 34217e4

Browse files
committed
Introduce support for text blocks in @CsvSource
@CsvSource allows users to provide CSV content as an array of strings, where each string represents a line in a CSV file. With the introduction of support for text blocks as a first-class language feature in recent JDKs (preview feature in Java SE 15), we can improve the user experience with @CsvSource by allowing the user to provide a text block instead of an array of strings. This commit introduces a new textBlock attribute in @CsvSource that allows users to take advantage of the text block support in their programming language. Given the following parameterized test using a text block... @ParameterizedTest @CsvSource(textBlock = """ apple, 1 banana, 2 'lemon, lime', 0xF1 strawberry, 700_000 """) void csvSourceWithTextBlock(String fruit, int rank) { System.out.println(fruit + " : " + rank); } ... the output is: apple : 1 banana : 2 lemon, lime : 241 strawberry : 700000 Closes #2721
1 parent 155a25f commit 34217e4

File tree

7 files changed

+186
-14
lines changed

7 files changed

+186
-14
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ GitHub.
4444
==== New Features and Improvements
4545

4646
* `JAVA_18` has been added to the `JRE` enum for use with JRE-based execution conditions.
47+
* CSV content in `@CsvSource` can now be supplied as a _text block_ instead of an array of
48+
strings. See the
49+
<<../user-guide/index.adoc#writing-tests-parameterized-tests-sources-CsvSource, User
50+
Guide>> for details and an example.
4751
* The `ExecutionMode` for the current test or container is now accessible via the
4852
`ExtensionContext`.
4953

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

+29-8
Original file line numberDiff line numberDiff line change
@@ -1330,14 +1330,34 @@ include::{testDir}/example/ExternalMethodSourceDemo.java[tags=external_MethodSou
13301330
[[writing-tests-parameterized-tests-sources-CsvSource]]
13311331
===== @CsvSource
13321332

1333-
`@CsvSource` allows you to express argument lists as comma-separated values (i.e.,
1334-
`String` literals).
1333+
`@CsvSource` allows you to express argument lists as comma-separated values (i.e., CSV
1334+
`String` literals). Each string provided via the `value` attribute in `@CsvSource`
1335+
represents a CSV line and results in one invocation of the parameterized test.
13351336

13361337
[source,java,indent=0]
13371338
----
13381339
include::{testDir}/example/ParameterizedTestDemo.java[tags=CsvSource_example]
13391340
----
13401341

1342+
If the programming language you are using supports _text blocks_ -- for example, Java SE
1343+
15 or higher -- you can alternatively use the `textBlock` attribute of `@CsvSource`. Each
1344+
line within a text block represents a CSV line and results in one invocation of the
1345+
parameterized test. Using a text block, the previous example can be implemented as follows.
1346+
1347+
[source,java,indent=0]
1348+
----
1349+
@ParameterizedTest
1350+
@CsvSource(textBlock = """
1351+
apple, 1
1352+
banana, 2
1353+
'lemon, lime', 0xF1
1354+
strawberry, 700_000
1355+
""")
1356+
void testWithCsvSource(String fruit, int rank) {
1357+
// ...
1358+
}
1359+
----
1360+
13411361
The default delimiter is a comma (`,`), but you can use another character by setting the
13421362
`delimiter` attribute. Alternatively, the `delimiterString` attribute allows you to use a
13431363
`String` delimiter instead of a single character. However, both delimiter attributes
@@ -1354,8 +1374,8 @@ reference is a primitive type.
13541374
NOTE: An _unquoted_ empty value will always be converted to a `null` reference regardless
13551375
of any custom values configured via the `nullValues` attribute.
13561376

1357-
Unless it starts with a quote character, leading and trailing whitespaces of a
1358-
CSV column are trimmed by default. This behavior can be changed by setting the
1377+
Unless it starts with a quote character, leading and trailing whitespace in a CSV column
1378+
is trimmed by default. This behavior can be changed by setting the
13591379
`ignoreLeadingAndTrailingWhitespace` attribute to `true`.
13601380

13611381
[cols="50,50"]
@@ -1373,8 +1393,9 @@ CSV column are trimmed by default. This behavior can be changed by setting the
13731393
[[writing-tests-parameterized-tests-sources-CsvFileSource]]
13741394
===== @CsvFileSource
13751395

1376-
`@CsvFileSource` lets you use CSV files from the classpath or the local file system. Each
1377-
line from a CSV file results in one invocation of the parameterized test.
1396+
`@CsvFileSource` lets you use comma-separated value (CSV) files from the classpath or the
1397+
local file system. Each line from a CSV file results in one invocation of the
1398+
parameterized test.
13781399

13791400
The default delimiter is a comma (`,`), but you can use another character by setting the
13801401
`delimiter` attribute. Alternatively, the `delimiterString` attribute allows you to use a
@@ -1407,8 +1428,8 @@ reference is a primitive type.
14071428
NOTE: An _unquoted_ empty value will always be converted to a `null` reference regardless
14081429
of any custom values configured via the `nullValues` attribute.
14091430

1410-
Unless it starts with a quote character, leading and trailing whitespaces of a
1411-
CSV column are trimmed by default. This behavior can be changed by setting the
1431+
Unless it starts with a quote character, leading and trailing whitespace in a CSV column
1432+
is trimmed by default. This behavior can be changed by setting the
14121433
`ignoreLeadingAndTrailingWhitespace` attribute to `true`.
14131434

14141435
[[writing-tests-parameterized-tests-sources-ArgumentsSource]]

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

+15-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.Arrays;
1818
import java.util.Set;
1919
import java.util.concurrent.atomic.AtomicLong;
20+
import java.util.regex.Pattern;
2021
import java.util.stream.Stream;
2122

2223
import com.univocity.parsers.csv.CsvParser;
@@ -32,6 +33,8 @@
3233
*/
3334
class CsvArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<CsvSource> {
3435

36+
private static final Pattern NEW_LINE_REGEX = Pattern.compile("\\n");
37+
3538
private static final String LINE_SEPARATOR = "\n";
3639

3740
private CsvSource annotation;
@@ -47,9 +50,20 @@ public void accept(CsvSource annotation) {
4750

4851
@Override
4952
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
53+
Preconditions.condition(this.annotation.value().length > 0 ^ !this.annotation.textBlock().isEmpty(),
54+
() -> "@CsvSource must be declared with either `value` or `textBlock` but not both");
55+
56+
String[] lines;
57+
if (!this.annotation.textBlock().isEmpty()) {
58+
lines = NEW_LINE_REGEX.split(this.annotation.textBlock(), 0);
59+
}
60+
else {
61+
lines = this.annotation.value();
62+
}
63+
5064
AtomicLong index = new AtomicLong(0);
5165
// @formatter:off
52-
return Arrays.stream(this.annotation.value())
66+
return Arrays.stream(lines)
5367
.map(line -> parseLine(index.getAndIncrement(), line))
5468
.map(Arguments::of);
5569
// @formatter:on

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

+62-4
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
import org.apiguardian.api.API;
2323

2424
/**
25-
* {@code @CsvSource} is an {@link ArgumentsSource} which reads
26-
* comma-separated values (CSV) from one or more supplied
27-
* {@linkplain #value CSV lines}.
25+
* {@code @CsvSource} is an {@link ArgumentsSource} which reads comma-separated
26+
* values (CSV) from one or more CSV lines supplied via the {@link #value}
27+
* attribute or {@link #textBlock} attribute.
2828
*
2929
* <p>The column delimiter (defaults to comma) can be customized with either
3030
* {@link #delimiter()} or {@link #delimiterString()}.
@@ -51,8 +51,65 @@
5151
* the specified {@link #delimiter} or {@link #delimiterString}. Any line
5252
* beginning with a {@code #} symbol will be interpreted as a comment and will
5353
* be ignored.
54+
*
55+
* <p>Defaults to an empty array. You therefore must supply CSV content
56+
* via this attribute or the {@link #textBlock} attribute.
57+
*
58+
* <p>If <em>text block</em> syntax is supported by your programming language,
59+
* you may find it more convenient to declare your CSV content via the
60+
* {@link #textBlock} attribute.
61+
*
62+
* <h4>Example</h4>
63+
* <pre class="code">
64+
* {@literal @}ParameterizedTest
65+
* {@literal @}CsvSource({
66+
* "apple, 1",
67+
* "banana, 2",
68+
* "'lemon, lime', 0xF1",
69+
* "strawberry, 700_000",
70+
* })
71+
* void test(String fruit, int rank) {
72+
* // ...
73+
* }</pre>
74+
*
75+
* @see #textBlock
5476
*/
55-
String[] value();
77+
String[] value() default {};
78+
79+
/**
80+
* The CSV lines to use as the source of arguments, supplied as a single
81+
* <em>text block</em>; must not be empty.
82+
*
83+
* <p>Each line in the text block corresponds to a line in a CSV file and will
84+
* be split using the specified {@link #delimiter} or {@link #delimiterString}.
85+
* Any line beginning with a {@code #} symbol will be interpreted as a comment
86+
* and will be ignored.
87+
*
88+
* <p>Defaults to an empty string. You therefore must supply CSV content
89+
* via this attribute or the {@link #value} attribute.
90+
*
91+
* <p>Text block syntax is supported by various languages on the JVM
92+
* including Java SE 15 or higher. If text blocks are not supported, you
93+
* should declare your CSV content via the {@link #value} attribute.
94+
*
95+
* <h4>Example</h4>
96+
* <pre class="code">
97+
* {@literal @}ParameterizedTest
98+
* {@literal @}CsvSource(textBlock = """
99+
* apple, 1
100+
* banana, 2
101+
* 'lemon, lime', 0xF1
102+
* strawberry, 700_000
103+
* """)
104+
* void test(String fruit, int rank) {
105+
* // ...
106+
* }</pre>
107+
*
108+
* @since 5.8.1
109+
* @see #value
110+
*/
111+
@API(status = EXPERIMENTAL, since = "5.8.1")
112+
String textBlock() default "";
56113

57114
/**
58115
* The column delimiter character to use when reading the {@linkplain #value lines}.
@@ -128,4 +185,5 @@
128185
*/
129186
@API(status = EXPERIMENTAL, since = "5.8")
130187
boolean ignoreLeadingAndTrailingWhitespace() default true;
188+
131189
}

junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java

+26
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,32 @@
9090
*/
9191
class ParameterizedTestIntegrationTests {
9292

93+
@ParameterizedTest
94+
@CsvSource(textBlock = """
95+
apple, 1
96+
banana, 2
97+
'lemon, lime', 0xF1
98+
strawberry, 700_000
99+
""")
100+
void executesLinesFromTextBlock(String fruit, int rank) {
101+
switch (fruit) {
102+
case "apple":
103+
assertThat(rank).isEqualTo(1);
104+
break;
105+
case "banana":
106+
assertThat(rank).isEqualTo(2);
107+
break;
108+
case "lemon, lime":
109+
assertThat(rank).isEqualTo(241);
110+
break;
111+
case "strawberry":
112+
assertThat(rank).isEqualTo(700_000);
113+
break;
114+
default:
115+
fail("Unexpected fruit : " + fruit);
116+
}
117+
}
118+
93119
@Test
94120
void executesWithSingleArgumentsProviderWithMultipleInvocations() {
95121
var results = execute("testWithTwoSingleStringArgumentsProvider", String.class);

junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java

+43-1
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,35 @@
2626
class CsvArgumentsProviderTests {
2727

2828
@Test
29-
void throwsExceptionOnInvalidCsv() {
29+
void throwsExceptionForInvalidCsv() {
3030
var annotation = csvSource("foo", "bar", "");
3131

3232
assertThatExceptionOfType(JUnitException.class)//
3333
.isThrownBy(() -> provideArguments(annotation).toArray())//
3434
.withMessage("Line at index 2 contains invalid CSV: \"\"");
3535
}
3636

37+
@Test
38+
void throwsExceptionIfNeitherValueNorTextBlockIsDeclared() {
39+
var annotation = csvSource().build();
40+
41+
assertThatExceptionOfType(PreconditionViolationException.class)//
42+
.isThrownBy(() -> provideArguments(annotation))//
43+
.withMessage("@CsvSource must be declared with either `value` or `textBlock` but not both");
44+
}
45+
46+
@Test
47+
void throwsExceptionIfValueAndTextBlockAreDeclared() {
48+
var annotation = csvSource().lines("foo").textBlock("""
49+
bar
50+
baz
51+
""").build();
52+
53+
assertThatExceptionOfType(PreconditionViolationException.class)//
54+
.isThrownBy(() -> provideArguments(annotation))//
55+
.withMessage("@CsvSource must be declared with either `value` or `textBlock` but not both");
56+
}
57+
3758
@Test
3859
void providesSingleArgument() {
3960
var annotation = csvSource("foo");
@@ -43,6 +64,15 @@ void providesSingleArgument() {
4364
assertThat(arguments).containsExactly(array("foo"));
4465
}
4566

67+
@Test
68+
void providesSingleArgumentFromTextBlock() {
69+
var annotation = csvSource().textBlock("foo").build();
70+
71+
var arguments = provideArguments(annotation);
72+
73+
assertThat(arguments).containsExactly(array("foo"));
74+
}
75+
4676
@Test
4777
void providesMultipleArguments() {
4878
var annotation = csvSource("foo", "bar");
@@ -52,6 +82,18 @@ void providesMultipleArguments() {
5282
assertThat(arguments).containsExactly(array("foo"), array("bar"));
5383
}
5484

85+
@Test
86+
void providesMultipleArgumentsFromTextBlock() {
87+
var annotation = csvSource().textBlock("""
88+
foo
89+
bar
90+
""").build();
91+
92+
var arguments = provideArguments(annotation);
93+
94+
assertThat(arguments).containsExactly(array("foo"), array("bar"));
95+
}
96+
5597
@Test
5698
void splitsAndTrimsArguments() {
5799
var annotation = csvSource(" foo , bar ");

junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/MockCsvAnnotationBuilder.java

+7
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ B ignoreLeadingAndTrailingWhitespace(boolean ignoreLeadingAndTrailingWhitespace)
8383
static class MockCsvSourceBuilder extends MockCsvAnnotationBuilder<CsvSource, MockCsvSourceBuilder> {
8484

8585
private String[] lines = new String[0];
86+
private String textBlock = "";
8687

8788
@Override
8889
protected MockCsvSourceBuilder getSelf() {
@@ -94,6 +95,11 @@ MockCsvSourceBuilder lines(String... lines) {
9495
return this;
9596
}
9697

98+
MockCsvSourceBuilder textBlock(String textBlock) {
99+
this.textBlock = textBlock;
100+
return this;
101+
}
102+
97103
@Override
98104
CsvSource build() {
99105
var annotation = mock(CsvSource.class);
@@ -108,6 +114,7 @@ CsvSource build() {
108114

109115
// @CsvSource
110116
when(annotation.value()).thenReturn(this.lines);
117+
when(annotation.textBlock()).thenReturn(this.textBlock);
111118

112119
return annotation;
113120
}

0 commit comments

Comments
 (0)