Skip to content

Commit 843e966

Browse files
committed
Support CharSequence argument for Fallback String-to-Object Conversion
Prior to this commit, the Fallback String-to-Object Conversion support for parameterized tests supported factory constructors and methods that accepted a single String argument. This commit relaxes that restriction to support factory constructors and methods that accept either a single String argument or a single CharSequence argument, since the original source String can always be supplied to an Executable that accepts a CharSequence. Note that this change is available to third parties via ConversionSupport in junit-platform-commons, which junit-jupiter-params utilizes. Closes #4815
1 parent 2c03c92 commit 843e966

File tree

6 files changed

+109
-21
lines changed

6 files changed

+109
-21
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-RC1.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ repository on GitHub.
2626
[[release-notes-6.0.0-RC1-junit-platform-new-features-and-improvements]]
2727
==== New Features and Improvements
2828

29-
* ❓
29+
* Convention-based conversion in `ConversionSupport` now supports factory methods and
30+
factory constructors that accept a single `CharSequence` argument in addition to the
31+
existing support for factories that accept a single `String` argument.
3032

3133

3234
[[release-notes-6.0.0-RC1-junit-jupiter]]
@@ -53,6 +55,10 @@ repository on GitHub.
5355
In addition, special characters are escaped within quoted text. Please refer to the
5456
<<../user-guide/index.adoc#writing-tests-parameterized-tests-display-names-quoted-text,
5557
User Guide>> for details.
58+
* <<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit-fallback,
59+
Fallback String-to-Object Conversion>> for parameterized tests now supports factory
60+
methods and factory constructors that accept a single `CharSequence` argument in
61+
addition to the existing support for factories that accept a single `String` argument.
5662

5763

5864
[[release-notes-6.0.0-RC1-junit-vintage]]

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2535,11 +2535,12 @@ table, JUnit Jupiter also provides a fallback mechanism for automatic conversion
25352535
method_ or a _factory constructor_ as defined below.
25362536

25372537
- __factory method__: a non-private, `static` method declared in the target type that
2538-
accepts a single `String` argument and returns an instance of the target type. The name
2539-
of the method can be arbitrary and need not follow any particular convention.
2538+
accepts either a single `String` argument or a single `CharSequence` argument and
2539+
returns an instance of the target type. The name of the method can be arbitrary and need
2540+
not follow any particular convention.
25402541
- __factory constructor__: a non-private constructor in the target type that accepts a
2541-
single `String` argument. Note that the target type must be declared as either a
2542-
top-level class or as a `static` nested class.
2542+
either a single `String` argument or a single `CharSequence` argument. Note that the
2543+
target type must be declared as either a top-level class or as a `static` nested class.
25432544

25442545
NOTE: If multiple _factory methods_ are discovered, they will be ignored. If a _factory
25452546
method_ and a _factory constructor_ are discovered, the factory method will be used

junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ private ConversionSupport() {
7575
*
7676
* <ol>
7777
* <li>Search for a single, non-private static factory method in the target
78-
* type that converts from a String to the target type. Use the factory method
79-
* if present.</li>
78+
* type that converts from a {@link String} or {@link CharSequence} to the
79+
* target type. Use the factory method if present.</li>
8080
* <li>Search for a single, non-private constructor in the target type that
81-
* accepts a String. Use the constructor if present.</li>
81+
* accepts a {@link String} or {@link CharSequence}. Use the constructor if
82+
* present.</li>
8283
* </ol>
8384
*
8485
* <p>If multiple suitable factory methods are discovered they will be ignored.

junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
package org.junit.platform.commons.support.conversion;
1212

1313
import static org.junit.platform.commons.support.HierarchyTraversalMode.BOTTOM_UP;
14-
import static org.junit.platform.commons.support.ModifierSupport.isNotPrivate;
1514
import static org.junit.platform.commons.support.ModifierSupport.isNotStatic;
15+
import static org.junit.platform.commons.support.ModifierSupport.isPrivate;
1616
import static org.junit.platform.commons.support.ReflectionSupport.findMethods;
1717
import static org.junit.platform.commons.support.ReflectionSupport.invokeMethod;
1818
import static org.junit.platform.commons.util.ReflectionUtils.findConstructors;
@@ -38,11 +38,11 @@
3838
* <h2>Search Algorithm</h2>
3939
*
4040
* <ol>
41-
* <li>Search for a single, non-private static factory method in the target
42-
* type that converts from a String to the target type. Use the factory method
43-
* if present.</li>
44-
* <li>Search for a single, non-private constructor in the target type that
45-
* accepts a String. Use the constructor if present.</li>
41+
* <li>Search for a single, non-private static factory method in the target type
42+
* that converts from a {@link String} or {@link CharSequence} to the target type.
43+
* Use the factory method if present.</li>
44+
* <li>Search for a single, non-private constructor in the target type that accepts
45+
* a {@link String} or {@link CharSequence}. Use the constructor if present.</li>
4646
* </ol>
4747
*
4848
* <p>If multiple suitable factory methods are discovered they will be ignored.
@@ -136,7 +136,7 @@ public boolean test(Method method) {
136136
if (isNotStatic(method)) {
137137
return false;
138138
}
139-
return isNotPrivateAndAcceptsSingleStringArgument(method);
139+
return isFactoryCandidate(method);
140140
}
141141

142142
}
@@ -160,15 +160,21 @@ public boolean test(Constructor<?> constructor) {
160160
if (!constructor.getDeclaringClass().equals(this.targetType)) {
161161
return false;
162162
}
163-
return isNotPrivateAndAcceptsSingleStringArgument(constructor);
163+
return isFactoryCandidate(constructor);
164164
}
165165

166166
}
167167

168-
private static boolean isNotPrivateAndAcceptsSingleStringArgument(Executable executable) {
169-
return isNotPrivate(executable) //
170-
&& (executable.getParameterCount() == 1) //
171-
&& (executable.getParameterTypes()[0] == String.class);
168+
/**
169+
* Determine if the supplied {@link Executable} is not private and accepts a
170+
* single {@link String} or {@link CharSequence} argument.
171+
*/
172+
private static boolean isFactoryCandidate(Executable executable) {
173+
if (isPrivate(executable) || (executable.getParameterCount() != 1)) {
174+
return false;
175+
}
176+
Object parameterType = executable.getParameterTypes()[0];
177+
return (parameterType == String.class || parameterType == CharSequence.class);
172178
}
173179

174180
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,17 @@ void executesWithImplicitGenericConverter() {
311311
event(test(), displayName("[2] book = book 2"), finishedWithFailure(message("book 2"))));
312312
}
313313

314+
/**
315+
* @since 6.0
316+
*/
317+
@Test
318+
void executesWithImplicitGenericConverterWithCharSequenceConstructor() {
319+
var results = execute("testWithImplicitGenericConverterWithCharSequenceConstructor", Record.class);
320+
results.testEvents().assertThatEvents() //
321+
.haveExactly(1, event(displayName("\"record 1\""), finishedWithFailure(message("record 1")))) //
322+
.haveExactly(1, event(displayName("\"record 2\""), finishedWithFailure(message("record 2"))));
323+
}
324+
314325
@Test
315326
void legacyReportingNames() {
316327
var results = execute("testWithCustomName", String.class, int.class);
@@ -1460,6 +1471,12 @@ void testWithImplicitGenericConverter(Book book) {
14601471
fail(book.title);
14611472
}
14621473

1474+
@ParameterizedTest(name = "{0}")
1475+
@ValueSource(strings = { "record 1", "record 2" })
1476+
void testWithImplicitGenericConverterWithCharSequenceConstructor(Record record) {
1477+
fail(record.title.toString());
1478+
}
1479+
14631480
@ParameterizedTest(quoteTextArguments = false)
14641481
@ValueSource(strings = { "O", "XXX" })
14651482
void testWithExplicitConverter(@ConvertWith(StringLengthConverter.class) int length) {
@@ -2673,6 +2690,9 @@ static Book factory(String title) {
26732690
}
26742691
}
26752692

2693+
record Record(CharSequence title) {
2694+
}
2695+
26762696
static class FailureInBeforeEachTestCase {
26772697

26782698
@BeforeEach

platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class FallbackStringToObjectConverterTests {
3838
@Test
3939
void isNotFactoryMethodForWrongParameterType() {
4040
assertThat(isBookFactoryMethod).rejects(bookMethod("factory", Object.class));
41+
assertThat(isBookFactoryMethod).rejects(bookMethod("factory", Number.class));
4142
}
4243

4344
@Test
@@ -55,6 +56,7 @@ void isFactoryMethodForValidMethods() {
5556
assertThat(isBookFactoryMethod).accepts(bookMethod("factory"));
5657
assertThat(new IsFactoryMethod(Newspaper.class)).accepts(newspaperMethod("from"), newspaperMethod("of"));
5758
assertThat(new IsFactoryMethod(Magazine.class)).accepts(magazineMethod("from"), magazineMethod("of"));
59+
assertThat(new IsFactoryMethod(Record2.class)).accepts(record2Method("from"));
5860
}
5961

6062
@Test
@@ -67,18 +69,30 @@ void isFactoryConstructorForValidConstructors() {
6769
assertThat(new IsFactoryConstructor(Book.class)).accepts(constructor(Book.class));
6870
assertThat(new IsFactoryConstructor(Journal.class)).accepts(constructor(Journal.class));
6971
assertThat(new IsFactoryConstructor(Newspaper.class)).accepts(constructor(Newspaper.class));
72+
assertThat(new IsFactoryConstructor(Record1.class)).accepts(Record1.class.getDeclaredConstructors()[0]);
73+
assertThat(new IsFactoryConstructor(Record2.class)).accepts(Record2.class.getDeclaredConstructors()[0]);
7074
}
7175

7276
@Test
7377
void convertsStringToBookViaStaticFactoryMethod() throws Exception {
7478
assertConverts("enigma", Book.class, Book.factory("enigma"));
7579
}
7680

81+
@Test
82+
void convertsStringToRecord2ViaStaticFactoryMethodAcceptingCharSequence() throws Exception {
83+
assertConvertsRecord2("enigma", Record2.from(new StringBuffer("enigma")));
84+
}
85+
7786
@Test
7887
void convertsStringToJournalViaFactoryConstructor() throws Exception {
7988
assertConverts("enigma", Journal.class, new Journal("enigma"));
8089
}
8190

91+
@Test
92+
void convertsStringToRecord1ViaFactoryConstructorAcceptingCharSequence() throws Exception {
93+
assertConvertsRecord1("enigma", new Record1(new StringBuffer("enigma")));
94+
}
95+
8296
@Test
8397
void convertsStringToNewspaperViaConstructorIgnoringMultipleFactoryMethods() throws Exception {
8498
assertConverts("enigma", Newspaper.class, new Newspaper("enigma"));
@@ -119,6 +133,10 @@ private static Method magazineMethod(String methodName) {
119133
return findMethod(Magazine.class, methodName, String.class).orElseThrow();
120134
}
121135

136+
private static Method record2Method(String methodName) {
137+
return findMethod(Record2.class, methodName, CharSequence.class).orElseThrow();
138+
}
139+
122140
private static void assertConverts(String input, Class<?> targetType, Object expectedOutput) throws Exception {
123141
assertThat(converter.canConvertTo(targetType)).isTrue();
124142

@@ -129,6 +147,27 @@ private static void assertConverts(String input, Class<?> targetType, Object exp
129147
.isEqualTo(expectedOutput);
130148
}
131149

150+
private static void assertConvertsRecord1(String input, Record1 expected) throws Exception {
151+
Class<?> targetType = Record1.class;
152+
153+
assertThat(converter.canConvertTo(targetType)).isTrue();
154+
155+
Record1 result = (Record1) converter.convert(input, targetType);
156+
157+
assertThat(result).isNotNull();
158+
assertThat(result.title.toString()).isEqualTo(expected.title.toString());
159+
}
160+
161+
private static void assertConvertsRecord2(String input, Record2 expected) throws Exception {
162+
Class<?> targetType = Record2.class;
163+
164+
assertThat(converter.canConvertTo(targetType)).isTrue();
165+
166+
Record2 result = (Record2) converter.convert(input, targetType);
167+
168+
assertThat(result).isEqualTo(expected);
169+
}
170+
132171
static class Book {
133172

134173
private final String title;
@@ -144,7 +183,12 @@ static Book factory(String title) {
144183

145184
// wrong parameter type
146185
static Book factory(Object obj) {
147-
return new Book(String.valueOf(obj));
186+
throw new UnsupportedOperationException();
187+
}
188+
189+
// wrong parameter type
190+
static Book factory(Number number) {
191+
throw new UnsupportedOperationException();
148192
}
149193

150194
@SuppressWarnings("unused")
@@ -231,6 +275,16 @@ static Magazine of(String title) {
231275

232276
}
233277

278+
record Record1(CharSequence title) {
279+
}
280+
281+
record Record2(String title) {
282+
283+
static Record2 from(CharSequence title) {
284+
return new Record2(title.toString());
285+
}
286+
}
287+
234288
static class Diary {
235289
}
236290

0 commit comments

Comments
 (0)