Skip to content

Commit 9a7a7fa

Browse files
committed
Contribute DefaultLocale and DefaultTimeZone
JUnit Pioneer is happy to contribute its DefaultLocaleExtension and DefaultTimeZoneExtension to JUnit Jupiter. closes #4727
1 parent 464022d commit 9a7a7fa

File tree

23 files changed

+2314
-6
lines changed

23 files changed

+2314
-6
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M2.adoc

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
*Scope:* ❓
77

88
For a complete list of all _closed_ issues and pull requests for this release, consult the
9-
link:{junit-framework-repo}+/milestone/112?closed=1+[6.1.0-M2] milestone page in the JUnit
10-
repository on GitHub.
11-
9+
link:{junit-framework-repo}+/milestone/112?closed=1+[6.1.0-M2] milestone page in the JUnit repository on GitHub.
1210

1311
[[release-notes-6.1.0-M2-junit-platform]]
1412
=== JUnit Platform
@@ -28,7 +26,6 @@ repository on GitHub.
2826

2927
* ❓
3028

31-
3229
[[release-notes-6.1.0-M2-junit-jupiter]]
3330
=== JUnit Jupiter
3431

@@ -45,8 +42,8 @@ repository on GitHub.
4542
[[release-notes-6.1.0-M2-junit-jupiter-new-features-and-improvements]]
4643
==== New Features and Improvements
4744

48-
*
49-
45+
* https://www.junit-pioneer.org/[JUnit Pioneer]'s `DefaultLocaleExtension` and `DefaultTimeZoneExtension` are now part of the JUnit Jupiter.
46+
Find an example at the <<../user-guide/index.adoc#writing-tests-built-in-extensions-DefaultLocaleAndTimeZone, User Guide>>.
5047

5148
[[release-notes-6.1.0-M2-junit-vintage]]
5249
=== JUnit Vintage

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3824,3 +3824,101 @@ include::{testDir}/example/AutoCloseDemo.java[tags=user_guide_example]
38243824
<1> Annotate an instance field with `@AutoClose`.
38253825
<2> `WebClient` implements `java.lang.AutoCloseable` which defines a `close()` method that
38263826
will be invoked after each `@Test` method.
3827+
3828+
[[writing-tests-built-in-extensions-DefaultLocaleAndTimeZone]]
3829+
==== The @DefaultLocale and @DefaultTimeZone Extensions
3830+
3831+
----
3832+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tags=default_locale_language]
3833+
----
3834+
3835+
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.
3836+
Both annotations work on the test class level and on the test method level, and are inherited from higher-level containers.
3837+
After the annotated element has been executed, the initial default value is restored.
3838+
3839+
===== `@DefaultLocale`
3840+
3841+
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]
3842+
3843+
[source,java,indent=0]
3844+
----
3845+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tags=default_locale_language]
3846+
----
3847+
3848+
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:
3849+
3850+
* `language` or
3851+
* `language` and `country` or
3852+
* `language`, `country`, and `variant`
3853+
3854+
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!
3855+
3856+
[source,java,indent=0]
3857+
----
3858+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_language_alternatives]
3859+
----
3860+
3861+
Note that mixing language tag configuration and constructor based configuration will cause an `ExtensionConfigurationException` to be thrown.
3862+
Furthermore, a `variant` can only be specified if `country` is also specified.
3863+
If `variant` is specified without `country`, an `ExtensionConfigurationException` will be thrown.
3864+
3865+
Any method level `@DefaultLocale` configurations will override class level configurations.
3866+
3867+
[source,java,indent=0]
3868+
----
3869+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_class_level]
3870+
----
3871+
3872+
NOTE: A class-level configuration means that the specified locale is set before and reset after each individual test in the annotated class.
3873+
3874+
If your use case is not covered, you can implement the `LocaleProvider` interface.
3875+
3876+
[source,java,indent=0]
3877+
----
3878+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_with_provider]
3879+
----
3880+
3881+
NOTE: The provider implementation must have a no-args (or the default) constructor.
3882+
3883+
===== `@DefaultTimeZone`
3884+
3885+
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.
3886+
3887+
[source,java,indent=0]
3888+
----
3889+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_zone]
3890+
----
3891+
3892+
Any method level `@DefaultTimeZone` configurations will override class level configurations:
3893+
3894+
[source,java,indent=0]
3895+
----
3896+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_class_level]
3897+
----
3898+
3899+
NOTE: A class-level configuration means that the specified time zone is set before and reset after each individual test in the annotated class.
3900+
3901+
If your use case is not covered, you can implement the `TimeZoneProvider` interface.
3902+
3903+
[source,java,indent=0]
3904+
----
3905+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_time_zone_with_provider]
3906+
----
3907+
3908+
NOTE: The provider implementation must have a no-args (or the default) constructor.
3909+
3910+
===== Thread-Safety
3911+
3912+
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.
3913+
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.
3914+
3915+
However, this does not cover all possible cases.
3916+
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.
3917+
Tests that cover code that reads or writes the default locale or time zone need to be annotated with the respective annotation:
3918+
3919+
* `@ReadsDefaultLocale`
3920+
* `@ReadsDefaultTimeZone`
3921+
* `@WritesDefaultLocale`
3922+
* `@WritesDefaultTimeZone`
3923+
3924+
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
exports org.junit.jupiter.api.extension.support;
3030
exports org.junit.jupiter.api.function;
3131
exports org.junit.jupiter.api.io;
32+
exports org.junit.jupiter.api.locale;
3233
exports org.junit.jupiter.api.parallel;
3334
exports org.junit.jupiter.api.timeout to org.junit.jupiter.engine;
35+
exports org.junit.jupiter.api.timezone;
3436

3537
opens org.junit.jupiter.api.condition to org.junit.platform.commons;
3638
}

0 commit comments

Comments
 (0)