Skip to content

Commit 85d2553

Browse files
authored
Convert strings to Locale using the IETF BCP 47 format by default
Strings parameters of parameterized tests are now converted using `Locale.forLanguageTag` by default. The `junit.jupiter.params.arguments.conversion.locale.format` configuration parameter allows to revert to the old behavior. Resolves junit-team#3141.
1 parent 8b71bfe commit 85d2553

File tree

10 files changed

+165
-15
lines changed

10 files changed

+165
-15
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc

+4-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ to start reporting discovery issues.
9898
- Blank `@SentenceFragment` declarations
9999
- `@BeforeParameterizedClassInvocation` and `@AfterParameterizedClassInvocation`
100100
methods declared in non-parameterized test classes
101-
101+
* `java.util.Locale` arguments are now converted according to the IETF BCP 47 language tag
102+
format. See the
103+
<<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>>
104+
for details.
102105

103106
[[release-notes-5.13.0-M3-junit-vintage]]
104107
=== JUnit Vintage

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -2483,10 +2483,16 @@ integral types: `byte`, `short`, `int`, `long`, and their boxed counterparts.
24832483
| `java.time.ZoneId` | `"Europe/Berlin"` -> `ZoneId.of("Europe/Berlin")`
24842484
| `java.time.ZoneOffset` | `"+02:30"` -> `ZoneOffset.ofHoursMinutes(2, 30)`
24852485
| `java.util.Currency` | `"JPY"` -> `Currency.getInstance("JPY")`
2486-
| `java.util.Locale` | `"en"` -> `new Locale("en")`
2486+
| `java.util.Locale` | `"en-US"` -> `Locale.forLanguageTag("en-US")`
24872487
| `java.util.UUID` | `"d043e930-7b3b-48e3-bdbe-5a3ccfb833db"` -> `UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")`
24882488
|===
24892489

2490+
WARNING: To revert to the old `java.util.Locale` conversion behavior of version 5.12 and
2491+
earlier (which called the deprecated `Locale(String)` constructor), you can set the
2492+
`junit.jupiter.params.arguments.conversion.locale.format`
2493+
<<running-tests-config-params, configuration parameter>> to `iso_639`. However, please
2494+
note that this parameter is deprecated and will be removed in a future release.
2495+
24902496
[[writing-tests-parameterized-tests-argument-conversion-implicit-fallback]]
24912497
====== Fallback String-to-Object Conversion
24922498

@@ -2523,7 +2529,7 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=implicit_fallback_con
25232529
[[writing-tests-parameterized-tests-argument-conversion-explicit]]
25242530
===== Explicit Conversion
25252531

2526-
Instead of relying on implicit argument conversion you may explicitly specify an
2532+
Instead of relying on implicit argument conversion, you may explicitly specify an
25272533
`ArgumentConverter` to use for a certain parameter using the `@ConvertWith` annotation
25282534
like in the following example. Note that an implementation of `ArgumentConverter` must be
25292535
declared as either a top-level class or as a `static` nested class.

junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private void storeParameterInfo(ExtensionContext context) {
7272
ParameterDeclarations declarations = this.declarationContext.getResolverFacade().getIndexedParameterDeclarations();
7373
ClassLoader classLoader = getClassLoader(this.declarationContext.getTestClass());
7474
Object[] arguments = this.arguments.getConsumedPayloads();
75-
ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments);
75+
ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments);
7676
new DefaultParameterInfo(declarations, accessor).store(context);
7777
}
7878

junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ private static Converter createConverter(ParameterDeclaration declaration, Exten
425425
.map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext))
426426
.map(converter -> AnnotationConsumerInitializer.initialize(declaration.getAnnotatedElement(), converter))
427427
.map(Converter::new)
428-
.orElse(Converter.DEFAULT);
428+
.orElseGet(() -> Converter.createDefault(extensionContext));
429429
} // @formatter:on
430430
catch (Exception ex) {
431431
throw parameterResolutionException("Error creating ArgumentConverter", ex, declaration.getParameterIndex());
@@ -467,10 +467,12 @@ Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, Eva
467467

468468
private static class Converter implements Resolver {
469469

470-
private static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE);
471-
472470
private final ArgumentConverter argumentConverter;
473471

472+
private static Converter createDefault(ExtensionContext context) {
473+
return new Converter(new DefaultArgumentConverter(context));
474+
}
475+
474476
Converter(ArgumentConverter argumentConverter) {
475477
this.argumentConverter = argumentConverter;
476478
}

junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.function.BiFunction;
2020

2121
import org.apiguardian.api.API;
22+
import org.junit.jupiter.api.extension.ExtensionContext;
2223
import org.junit.jupiter.params.converter.DefaultArgumentConverter;
2324
import org.junit.platform.commons.util.ClassUtils;
2425
import org.junit.platform.commons.util.Preconditions;
@@ -40,10 +41,11 @@ public class DefaultArgumentsAccessor implements ArgumentsAccessor {
4041
private final Object[] arguments;
4142
private final BiFunction<Object, Class<?>, Object> converter;
4243

43-
public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader classLoader, Object[] arguments) {
44+
public static DefaultArgumentsAccessor create(ExtensionContext context, int invocationIndex,
45+
ClassLoader classLoader, Object[] arguments) {
4446
Preconditions.notNull(classLoader, "ClassLoader must not be null");
4547

46-
BiFunction<Object, Class<?>, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE //
48+
BiFunction<Object, Class<?>, Object> converter = (source, targetType) -> new DefaultArgumentConverter(context) //
4749
.convert(source, targetType, classLoader);
4850
return new DefaultArgumentsAccessor(converter, invocationIndex, arguments);
4951
}

junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java

+43-3
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import java.util.Currency;
2222
import java.util.Locale;
2323
import java.util.UUID;
24+
import java.util.function.Function;
2425

2526
import org.apiguardian.api.API;
27+
import org.junit.jupiter.api.extension.ExtensionContext;
2628
import org.junit.jupiter.api.extension.ParameterContext;
2729
import org.junit.jupiter.params.support.FieldContext;
2830
import org.junit.platform.commons.support.conversion.ConversionException;
@@ -50,10 +52,31 @@
5052
@API(status = INTERNAL, since = "5.0")
5153
public class DefaultArgumentConverter implements ArgumentConverter {
5254

53-
public static final DefaultArgumentConverter INSTANCE = new DefaultArgumentConverter();
55+
/**
56+
* Property name used to set the format for the conversion of {@link Locale}
57+
* arguments: {@value}
58+
*
59+
* <h4>Supported Values</h4>
60+
* <ul>
61+
* <li>{@code bcp_47}: uses the IETF BCP 47 language tag format, delegating
62+
* the conversion to {@link Locale#forLanguageTag(String)}</li>
63+
* <li>{@code iso_639}: uses the ISO 639 alpha-2 or alpha-3 language code
64+
* format, delegating the conversion to {@link Locale#Locale(String)}</li>
65+
* </ul>
66+
*
67+
* <p>If not specified, the default is {@code bcp_47}.
68+
*
69+
* @since 5.13
70+
*/
71+
public static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format";
5472

55-
private DefaultArgumentConverter() {
56-
// nothing to initialize
73+
private static final Function<String, LocaleConversionFormat> TRANSFORMER = value -> LocaleConversionFormat.valueOf(
74+
value.trim().toUpperCase(Locale.ROOT));
75+
76+
private final ExtensionContext context;
77+
78+
public DefaultArgumentConverter(ExtensionContext context) {
79+
this.context = context;
5780
}
5881

5982
@Override
@@ -84,6 +107,10 @@ public final Object convert(Object source, Class<?> targetType, ClassLoader clas
84107
}
85108

86109
if (source instanceof String) {
110+
if (targetType == Locale.class && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) {
111+
return Locale.forLanguageTag((String) source);
112+
}
113+
87114
try {
88115
return convert((String) source, targetType, classLoader);
89116
}
@@ -97,8 +124,21 @@ public final Object convert(Object source, Class<?> targetType, ClassLoader clas
97124
source.getClass().getTypeName(), targetType.getTypeName()));
98125
}
99126

127+
private LocaleConversionFormat getLocaleConversionFormat() {
128+
return context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, TRANSFORMER) //
129+
.orElse(LocaleConversionFormat.BCP_47);
130+
}
131+
100132
Object convert(String source, Class<?> targetType, ClassLoader classLoader) {
101133
return ConversionSupport.convert(source, targetType, classLoader);
102134
}
103135

136+
enum LocaleConversionFormat {
137+
138+
BCP_47,
139+
140+
ISO_639
141+
142+
}
143+
104144
}

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

+50
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
2323
import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.appendTestTemplateInvocationSegment;
2424
import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod;
25+
import static org.junit.jupiter.params.converter.DefaultArgumentConverter.DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME;
2526
import static org.junit.jupiter.params.provider.Arguments.arguments;
2627
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
2728
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration;
@@ -476,6 +477,29 @@ void failsWhenNoArgumentsSourceIsDeclared() {
476477
"Configuration error: You must configure at least one arguments source for this @ParameterizedTest"))));
477478
}
478479

480+
@Test
481+
void executesWithDefaultLocaleConversionFormat() {
482+
var results = execute(LocaleConversionTestCase.class, "testWithBcp47", Locale.class);
483+
484+
results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
485+
}
486+
487+
@Test
488+
void executesWithBcp47LocaleConversionFormat() {
489+
var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "bcp_47"),
490+
LocaleConversionTestCase.class, "testWithBcp47", Locale.class);
491+
492+
results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
493+
}
494+
495+
@Test
496+
void executesWithIso639LocaleConversionFormat() {
497+
var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "iso_639"),
498+
LocaleConversionTestCase.class, "testWithIso639", Locale.class);
499+
500+
results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
501+
}
502+
479503
private EngineExecutionResults execute(DiscoverySelector... selectors) {
480504
return EngineTestKit.engine(new JupiterTestEngine()).selectors(selectors).execute();
481505
}
@@ -484,6 +508,14 @@ private EngineExecutionResults execute(Class<?> testClass, String methodName, Cl
484508
return execute(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes)));
485509
}
486510

511+
private EngineExecutionResults execute(Map<String, String> configurationParameters, Class<?> testClass,
512+
String methodName, Class<?>... methodParameterTypes) {
513+
return EngineTestKit.engine(new JupiterTestEngine()) //
514+
.selectors(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes))) //
515+
.configurationParameters(configurationParameters) //
516+
.execute();
517+
}
518+
487519
private EngineExecutionResults execute(String methodName, Class<?>... methodParameterTypes) {
488520
return execute(TestCase.class, methodName, methodParameterTypes);
489521
}
@@ -2508,6 +2540,24 @@ public static Stream<Arguments> zeroArgumentsProvider() {
25082540
}
25092541
}
25102542

2543+
static class LocaleConversionTestCase {
2544+
2545+
@ParameterizedTest
2546+
@ValueSource(strings = "en-US")
2547+
void testWithBcp47(Locale locale) {
2548+
assertEquals("en", locale.getLanguage());
2549+
assertEquals("US", locale.getCountry());
2550+
}
2551+
2552+
@ParameterizedTest
2553+
@ValueSource(strings = "en-US")
2554+
void testWithIso639(Locale locale) {
2555+
assertEquals("en-us", locale.getLanguage());
2556+
assertEquals("", locale.getCountry());
2557+
}
2558+
2559+
}
2560+
25112561
private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider {
25122562

25132563
@Override

jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
1717
import static org.junit.jupiter.api.Assertions.assertNull;
1818
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.mockito.Mockito.mock;
1920

2021
import java.util.Arrays;
2122

2223
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtensionContext;
2325
import org.junit.platform.commons.PreconditionViolationException;
2426

2527
/**
@@ -164,8 +166,9 @@ void size() {
164166
}
165167

166168
private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationIndex, Object... arguments) {
169+
var context = mock(ExtensionContext.class);
167170
var classLoader = DefaultArgumentsAccessorTests.class.getClassLoader();
168-
return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments);
171+
return DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments);
169172
}
170173

171174
@SuppressWarnings("unused")

jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java

+42-1
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,25 @@
1212

1313
import static org.assertj.core.api.Assertions.assertThat;
1414
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
15+
import static org.junit.jupiter.params.converter.DefaultArgumentConverter.DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME;
16+
import static org.junit.jupiter.params.converter.DefaultArgumentConverter.LocaleConversionFormat.BCP_47;
17+
import static org.junit.jupiter.params.converter.DefaultArgumentConverter.LocaleConversionFormat.ISO_639;
1518
import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader;
1619
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.ArgumentMatchers.eq;
1721
import static org.mockito.Mockito.doReturn;
1822
import static org.mockito.Mockito.doThrow;
23+
import static org.mockito.Mockito.mock;
1924
import static org.mockito.Mockito.never;
2025
import static org.mockito.Mockito.spy;
2126
import static org.mockito.Mockito.verify;
27+
import static org.mockito.Mockito.when;
28+
29+
import java.util.Locale;
30+
import java.util.Optional;
2231

2332
import org.junit.jupiter.api.Test;
33+
import org.junit.jupiter.api.extension.ExtensionContext;
2434
import org.junit.jupiter.params.ParameterizedTest;
2535
import org.junit.jupiter.params.provider.ValueSource;
2636
import org.junit.platform.commons.support.ReflectionSupport;
@@ -35,7 +45,8 @@
3545
*/
3646
class DefaultArgumentConverterTests {
3747

38-
private final DefaultArgumentConverter underTest = spy(DefaultArgumentConverter.INSTANCE);
48+
private final ExtensionContext context = mock();
49+
private final DefaultArgumentConverter underTest = spy(new DefaultArgumentConverter(context));
3950

4051
@Test
4152
void isAwareOfNull() {
@@ -100,6 +111,36 @@ void delegatesStringsConversion() {
100111
verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class));
101112
}
102113

114+
@Test
115+
void convertsLocaleWithDefaultFormat() {
116+
when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) //
117+
.thenReturn(Optional.empty());
118+
119+
assertConverts("en", Locale.class, Locale.ENGLISH);
120+
assertConverts("en-US", Locale.class, Locale.US);
121+
}
122+
123+
@Test
124+
void convertsLocaleWithExplicitBcp47Format() {
125+
when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) //
126+
.thenReturn(Optional.of(BCP_47));
127+
128+
assertConverts("en", Locale.class, Locale.ENGLISH);
129+
assertConverts("en-US", Locale.class, Locale.US);
130+
}
131+
132+
@Test
133+
void delegatesLocaleConversionWithExplicitIso639Format() {
134+
when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) //
135+
.thenReturn(Optional.of(ISO_639));
136+
137+
doReturn(null).when(underTest).convert(any(), any(), any(ClassLoader.class));
138+
139+
convert("en", Locale.class);
140+
141+
verify(underTest).convert("en", Locale.class, getClassLoader(DefaultArgumentConverterTests.class));
142+
}
143+
103144
@Test
104145
void throwsExceptionForDelegatedConversionFailure() {
105146
ConversionException exception = new ConversionException("fail");

jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import org.assertj.core.api.Assertions.assertThat
1313
import org.junit.jupiter.api.Assertions.assertEquals
1414
import org.junit.jupiter.api.Test
1515
import org.junit.jupiter.api.assertThrows
16+
import org.junit.jupiter.api.extension.ExtensionContext
17+
import org.mockito.Mockito.mock
1618

1719
/**
1820
* Unit tests for using [ArgumentsAccessor] from Kotlin.
@@ -52,8 +54,9 @@ class ArgumentsAccessorKotlinTests {
5254
invocationIndex: Int,
5355
vararg arguments: Any
5456
): DefaultArgumentsAccessor {
57+
val context = mock(ExtensionContext::class.java)
5558
val classLoader = ArgumentsAccessorKotlinTests::class.java.classLoader
56-
return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments)
59+
return DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments)
5760
}
5861

5962
fun foo() {

0 commit comments

Comments
 (0)