Skip to content

Commit 91acc97

Browse files
committed
Combine for easier rebase
1 parent 19557e4 commit 91acc97

21 files changed

+2298
-0
lines changed

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3826,3 +3826,101 @@ include::{testDir}/example/AutoCloseDemo.java[tags=user_guide_example]
38263826
<1> Annotate an instance field with `@AutoClose`.
38273827
<2> `WebClient` implements `java.lang.AutoCloseable` which defines a `close()` method that
38283828
will be invoked after each `@Test` method.
3829+
3830+
[[writing-tests-built-in-extensions-DefaultLocaleAndTimeZone]]
3831+
==== The @DefaultLocale and @DefaultTimeZone Extensions
3832+
3833+
----
3834+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tags=default_locale_language]
3835+
----
3836+
3837+
The `@DefaultLocale` and `@DefaultTimeZone` annotations can be used to change the values returned from `Locale.getDefault()` and `TimeZone.getDefault()`, respectively, which are often used implicitly when no specific locale or time zone is chosen.
3838+
Both annotations work on the test class level and on the test method level, and are inherited from higher-level containers.
3839+
After the annotated element has been executed, the initial default value is restored.
3840+
3841+
===== `@DefaultLocale`
3842+
3843+
The default `Locale` can be specified using an {jdk-javadoc-base-url}/java.base/java/util/Locale.html#forLanguageTag-java.lang.String-[IETF BCP 47 language tag string]
3844+
3845+
[source,java,indent=0]
3846+
----
3847+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tags=default_locale_language]
3848+
----
3849+
3850+
Alternatively the default `Locale` can be created using the following attributes of which a {jdk-javadoc-base-url}/java.base/java/util/Locale.Builder.html[Locale Builder] can create an instance with:
3851+
3852+
* `language` or
3853+
* `language` and `country` or
3854+
* `language`, `country`, and `variant`
3855+
3856+
NOTE: The variant needs to be a string which follows the https://www.rfc-editor.org/rfc/rfc5646.html[IETF BCP 47 / RFC 5646] syntax!
3857+
3858+
[source,java,indent=0]
3859+
----
3860+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_language_alternatives]
3861+
----
3862+
3863+
Note that mixing language tag configuration and constructor based configuration will cause an `ExtensionConfigurationException` to be thrown.
3864+
Furthermore, a `variant` can only be specified if `country` is also specified.
3865+
If `variant` is specified without `country`, an `ExtensionConfigurationException` will be thrown.
3866+
3867+
Any method level `@DefaultLocale` configurations will override class level configurations.
3868+
3869+
[source,java,indent=0]
3870+
----
3871+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_class_level]
3872+
----
3873+
3874+
NOTE: A class-level configuration means that the specified locale is set before and reset after each individual test in the annotated class.
3875+
3876+
If your use case is not covered, you can implement the `LocaleProvider` interface.
3877+
3878+
[source,java,indent=0]
3879+
----
3880+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_with_provider]
3881+
----
3882+
3883+
NOTE: The provider implementation must have a no-args (or the default) constructor.
3884+
3885+
===== `@DefaultTimeZone`
3886+
3887+
The default `TimeZone` is specified according to the https://docs.oracle.com/javase/8/docs/api/java/util/TimeZone.html#getTimeZone-java.lang.String-[TimeZone.getTimeZone(String)] method.
3888+
3889+
[source,java,indent=0]
3890+
----
3891+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_zone]
3892+
----
3893+
3894+
Any method level `@DefaultTimeZone` configurations will override class level configurations:
3895+
3896+
[source,java,indent=0]
3897+
----
3898+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_class_level]
3899+
----
3900+
3901+
NOTE: A class-level configuration means that the specified time zone is set before and reset after each individual test in the annotated class.
3902+
3903+
If your use case is not covered, you can implement the `TimeZoneProvider` interface.
3904+
3905+
[source,java,indent=0]
3906+
----
3907+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_time_zone_with_provider]
3908+
----
3909+
3910+
NOTE: The provider implementation must have a no-args (or the default) constructor.
3911+
3912+
===== Thread-Safety
3913+
3914+
Since default locale and time zone are global state, reading and writing them during https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution[parallel test execution] can lead to unpredictable results and flaky tests.
3915+
The `@DefaultLocale` and `@DefaultTimeZone` extensions are prepared for that and tests annotated with them will never execute in parallel (thanks to https://docs.junit.org/current/api/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLock.html[resource locks]) to guarantee correct test results.
3916+
3917+
However, this does not cover all possible cases.
3918+
Tested code that reads or writes default locale and time zone _independently_ of the extensions can still run in parallel to them and may thus behave erratically when, for example, it unexpectedly reads a locale set by the extension in another thread.
3919+
Tests that cover code that reads or writes the default locale or time zone need to be annotated with the respective annotation:
3920+
3921+
* `@ReadsDefaultLocale`
3922+
* `@ReadsDefaultTimeZone`
3923+
* `@WritesDefaultLocale`
3924+
* `@WritesDefaultTimeZone`
3925+
3926+
Tests annotated in this way will never execute in parallel with tests annotated with `@DefaultLocale` or `@DefaultTimeZone`.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
import java.time.ZoneOffset;
16+
import java.util.Locale;
17+
import java.util.TimeZone;
18+
19+
import org.junit.jupiter.api.Nested;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.locale.DefaultLocale;
22+
import org.junit.jupiter.api.locale.LocaleProvider;
23+
import org.junit.jupiter.api.timezone.DefaultTimeZone;
24+
import org.junit.jupiter.api.timezone.TimeZoneProvider;
25+
26+
public class DefaultLocaleTimezoneExtensionDemo {
27+
28+
// tag::default_locale_language[]
29+
@Test
30+
@DefaultLocale("zh-Hant-TW")
31+
void test_with_language() {
32+
assertThat(Locale.getDefault()).isEqualTo(Locale.forLanguageTag("zh-Hant-TW"));
33+
}
34+
// end::default_locale_language[]
35+
36+
// tag::default_locale_language_alternatives[]
37+
@Test
38+
@DefaultLocale(language = "en")
39+
void test_with_language_only() {
40+
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
41+
}
42+
43+
@Test
44+
@DefaultLocale(language = "en", country = "EN")
45+
void test_with_language_and_country() {
46+
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").setRegion("EN").build());
47+
}
48+
49+
@Test
50+
@DefaultLocale(language = "ja", country = "JP", variant = "japanese")
51+
void test_with_language_and_country_and_vairant() {
52+
assertThat(Locale.getDefault()).isEqualTo(
53+
new Locale.Builder().setLanguage("ja").setRegion("JP").setVariant("japanese").build());
54+
}
55+
// end::default_locale_language_alternatives[]
56+
57+
@Nested
58+
// tag::default_locale_class_level[]
59+
@DefaultLocale(language = "fr")
60+
class MyLocaleTests {
61+
62+
@Test
63+
void test_with_class_level_configuration() {
64+
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("fr").build());
65+
}
66+
67+
@Test
68+
@DefaultLocale(language = "en")
69+
void test_with_method_level_configuration() {
70+
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
71+
}
72+
73+
}
74+
// end::default_locale_class_level[]
75+
76+
// tag::default_locale_with_provider[]
77+
@Test
78+
@DefaultLocale(localeProvider = EnglishProvider.class)
79+
void test_with_locale_provider() {
80+
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
81+
}
82+
83+
static class EnglishProvider implements LocaleProvider {
84+
85+
@Override
86+
public Locale get() {
87+
return Locale.ENGLISH;
88+
}
89+
90+
}
91+
// end::default_locale_with_provider[]
92+
93+
// tag::default_timezone_zone[]
94+
@Test
95+
@DefaultTimeZone("CET")
96+
void test_with_short_zone_id() {
97+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
98+
}
99+
100+
@Test
101+
@DefaultTimeZone("Africa/Juba")
102+
void test_with_long_zone_id() {
103+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
104+
}
105+
// end::default_timezone_zone[]
106+
107+
@Nested
108+
// tag::default_timezone_class_level[]
109+
@DefaultTimeZone("CET")
110+
class MyTimeZoneTests {
111+
112+
@Test
113+
void test_with_class_level_configuration() {
114+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
115+
}
116+
117+
@Test
118+
@DefaultTimeZone("Africa/Juba")
119+
void test_with_method_level_configuration() {
120+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
121+
}
122+
123+
}
124+
// end::default_timezone_class_level[]
125+
126+
// tag::default_time_zone_with_provider[]
127+
@Test
128+
@DefaultTimeZone(timeZoneProvider = UtcTimeZoneProvider.class)
129+
void test_with_time_zone_provider() {
130+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("UTC"));
131+
}
132+
133+
static class UtcTimeZoneProvider implements TimeZoneProvider {
134+
135+
@Override
136+
public TimeZone get() {
137+
return TimeZone.getTimeZone(ZoneOffset.UTC);
138+
}
139+
140+
}
141+
// end::default_time_zone_with_provider[]
142+
143+
}

junit-jupiter-api/src/main/java/module-info.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
exports org.junit.jupiter.api.io;
3232
exports org.junit.jupiter.api.parallel;
3333
exports org.junit.jupiter.api.timeout to org.junit.jupiter.engine;
34+
exports org.junit.jupiter.api.locale to org.junit.platform.commons;
35+
exports org.junit.jupiter.api.timezone to org.junit.platform.commons;
3436

3537
opens org.junit.jupiter.api.condition to org.junit.platform.commons;
38+
opens org.junit.jupiter.api.timeout to org.junit.platform.commons;
39+
opens org.junit.jupiter.api.locale to org.junit.platform.commons;
40+
opens org.junit.jupiter.api.timezone to org.junit.platform.commons;
3641
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api.locale;
12+
13+
import static org.apiguardian.api.API.Status.STABLE;
14+
15+
import java.lang.annotation.ElementType;
16+
import java.lang.annotation.Inherited;
17+
import java.lang.annotation.Retention;
18+
import java.lang.annotation.RetentionPolicy;
19+
import java.lang.annotation.Target;
20+
21+
import org.apiguardian.api.API;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
import org.junit.jupiter.api.timezone.DefaultTimeZone;
24+
25+
/**
26+
* {@code @DefaultLocale} is a JUnit Jupiter extension to change the value
27+
* returned by {@link java.util.Locale#getDefault()} for a test execution.
28+
*
29+
* <p>The {@link java.util.Locale} to set as the default locale can be
30+
* configured in several ways:</p>
31+
*
32+
* <ul>
33+
* <li>using a {@link java.util.Locale#forLanguageTag(String) language tag}</li>
34+
* <li>using a {@link java.util.Locale.Builder Locale.Builder} together with
35+
* <ul>
36+
* <li>a language</li>
37+
* <li>a language and a county</li>
38+
* <li>a language, a county, and a variant</li>
39+
* </ul>
40+
* </li>
41+
* </ul>
42+
*
43+
* <p>Please keep in mind that the {@code Locale.Builder} does a syntax check, if you use a variant!
44+
* The given string must match the BCP 47 (or more detailed <a href="https://www.rfc-editor.org/rfc/rfc5646.html">RFC 5646</a>) syntax.</p>
45+
*
46+
* <p>If a language tag is set, none of the other fields must be set. Otherwise, an
47+
* {@link org.junit.jupiter.api.extension.ExtensionConfigurationException} will
48+
* be thrown. Specifying a {@link #country()} but no {@link #language()}, or a
49+
* {@link #variant()} but no {@link #country()} and {@link #language()} will
50+
* also cause an {@code ExtensionConfigurationException}. After the annotated
51+
* element has been executed, the default {@code Locale} will be restored to
52+
* its original value.</p>
53+
*
54+
* <p>{@code @DefaultLocale} can be used on the method and on the class level. It
55+
* is inherited from higher-level containers, but can only be used once per method
56+
* or class. If a class is annotated, the configured {@code Locale} will be the
57+
* default {@code Locale} for all tests inside that class. Any method level
58+
* configurations will override the class level default {@code Locale}.</p>
59+
*
60+
* <p>During
61+
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
62+
* all tests annotated with {@link DefaultLocale}, {@link ReadsDefaultLocale}, and {@link WritesDefaultLocale}
63+
* are scheduled in a way that guarantees correctness under mutation of shared global state.</p>
64+
*
65+
* <p>For more details and examples, see
66+
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-built-in-extensions-DefaultLocalAndTimezone" target="_top">the documentation on <code>@DefaultLocale</code> and <code>@DefaultTimeZone</code></a>.</p>
67+
*
68+
* @since 6.1
69+
* @see java.util.Locale#getDefault()
70+
* @see DefaultTimeZone
71+
*/
72+
@Retention(RetentionPolicy.RUNTIME)
73+
@Target({ ElementType.METHOD, ElementType.TYPE })
74+
@Inherited
75+
@WritesDefaultLocale
76+
@API(status = STABLE, since = "6.1")
77+
@ExtendWith(DefaultLocaleExtension.class)
78+
public @interface DefaultLocale {
79+
80+
/**
81+
* A language tag string as specified by IETF BCP 47. See
82+
* {@link java.util.Locale#forLanguageTag(String)} for more information
83+
* about valid language tag values.
84+
*
85+
* @since 0.3
86+
*/
87+
String value() default "";
88+
89+
/**
90+
* An ISO 639 alpha-2 or alpha-3 language code, or a language subtag up to
91+
* 8 characters in length. See the {@link java.util.Locale} class
92+
* description about valid language values.
93+
*/
94+
String language() default "";
95+
96+
/**
97+
* An ISO 3166 alpha-2 country code or a UN M.49 numeric-3 area code. See
98+
* the {@link java.util.Locale} class description about valid country
99+
* values.
100+
*/
101+
String country() default "";
102+
103+
/**
104+
* An IETF BCP 47 language string that matches the <a href="https://www.rfc-editor.org/rfc/rfc5646.html">RFC 5646</a> syntax.
105+
* It's validated by the {@code Locale.Builder}, using {@code sun.util.locale.LanguageTag#isVariant}.
106+
*/
107+
String variant() default "";
108+
109+
/**
110+
* A class implementing {@link LocaleProvider} to be used for custom {@code Locale} resolution.
111+
* This is mutually exclusive with other properties, if any other property is given a value it
112+
* will result in an {@link org.junit.jupiter.api.extension.ExtensionConfigurationException}.
113+
*/
114+
Class<? extends LocaleProvider> localeProvider() default LocaleProvider.NullLocaleProvider.class;
115+
116+
}

0 commit comments

Comments
 (0)