Skip to content

Commit bea12a8

Browse files
Bukamamarcphilipp
andauthored
Contribute @DefaultLocale and @DefaultTimeZone extensions (#5142)
JUnit Pioneer is happy to contribute its `DefaultLocaleExtension` and `DefaultTimeZoneExtension` to JUnit Jupiter. Resolves #4727. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent da338b3 commit bea12a8

File tree

22 files changed

+1975
-3
lines changed

22 files changed

+1975
-3
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ endif::[]
133133
:TestMethodOrder: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/TestMethodOrder.html[@TestMethodOrder]
134134
:TestReporter: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/TestReporter.html[TestReporter]
135135
:TestTemplate: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/TestTemplate.html[@TestTemplate]
136+
// @DefaultLocale and @DefaultTimeZone
137+
:DefaultLocale: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/DefaultLocale.html[@DefaultLocale]
138+
:DefaultTimeZone: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/DefaultTimeZone.html[@DefaultTimeZone]
139+
:LocaleProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/LocaleProvider.html[LocaleProvider]
140+
:TimeZoneProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/TimeZoneProvider.html[TimeZoneProvider]
141+
:ReadsDefaultLocale: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/ReadsDefaultLocale.html[@ReadsDefaultLocale]
142+
:ReadsDefaultTimeZone: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/ReadsDefaultTimeZone.html[@ReadsDefaultTimeZone]
143+
:WritesDefaultLocale: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/WritesDefaultLocale.html[@WritesDefaultLocale]
144+
:WritesDefaultTimeZone: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/WritesDefaultTimeZone.html[@WritesDefaultTimeZone]
136145
// Jupiter Parallel API
137146
:Execution: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Execution.html[@Execution]
138147
:Isolated: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Isolated.html[@Isolated]

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ For a complete list of all _closed_ issues and pull requests for this release, c
99
link:{junit-framework-repo}+/milestone/112?closed=1+[6.1.0-M2] milestone page in the JUnit
1010
repository on GitHub.
1111

12-
1312
[[release-notes-6.1.0-M2-junit-platform]]
1413
=== JUnit Platform
1514

@@ -28,7 +27,6 @@ repository on GitHub.
2827

2928
* ❓
3029

31-
3230
[[release-notes-6.1.0-M2-junit-jupiter]]
3331
=== JUnit Jupiter
3432

@@ -46,7 +44,9 @@ repository on GitHub.
4644
==== New Features and Improvements
4745

4846
* `JAVA_27` has been added to the `JRE` enum for use with `JRE`-based execution conditions.
49-
47+
* https://www.junit-pioneer.org/[JUnit Pioneer]'s `DefaultLocaleExtension` and
48+
`DefaultTimeZoneExtension` are now part of the JUnit Jupiter. Find examples in the
49+
<<../user-guide/index.adoc#writing-tests-built-in-extensions-DefaultLocaleAndTimeZone, User Guide>>.
5050

5151
[[release-notes-6.1.0-M2-junit-vintage]]
5252
=== JUnit Vintage

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

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3824,3 +3824,117 @@ 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+
The `{DefaultLocale}` and `{DefaultTimeZone}` annotations can be used to change the values
3832+
returned from `Locale.getDefault()` and `TimeZone.getDefault()`, respectively, which are
3833+
often used implicitly when no specific locale or time zone is chosen. Both annotations
3834+
work on the test class level and on the test method level, and are inherited from
3835+
higher-level containers. After the annotated element has been executed, the initial
3836+
default value is restored.
3837+
3838+
[[writing-tests-built-in-extensions-DefaultLocale]]
3839+
===== @DefaultLocale
3840+
3841+
The default `Locale` can be specified using an
3842+
{jdk-javadoc-base-url}/java.base/java/util/Locale.html#forLanguageTag-java.lang.String-[IETF BCP 47 language tag string].
3843+
3844+
[source,java,indent=0]
3845+
----
3846+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tags=default_locale_language]
3847+
----
3848+
3849+
Alternatively, the default `Locale` can be created using the following attributes from
3850+
which a {jdk-javadoc-base-url}/java.base/java/util/Locale.Builder.html[`Locale.Builder`]
3851+
can create an instance:
3852+
3853+
* `language` or
3854+
* `language` and `country` or
3855+
* `language`, `country`, and `variant`
3856+
3857+
NOTE: The variant needs to be a string which follows the
3858+
https://www.rfc-editor.org/rfc/rfc5646.html[IETF BCP 47 / RFC 5646] syntax
3859+
3860+
[source,java,indent=0]
3861+
----
3862+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_language_alternatives]
3863+
----
3864+
3865+
Mixing language tag configuration (via the annotation's `value` attributed) and
3866+
attributed-based configuration will cause an exception to be thrown. Furthermore, a
3867+
`variant` can only be specified if `country` is also specified. Otherwise, an exception
3868+
will be thrown.
3869+
3870+
Any method-level `@DefaultLocale` configurations will override class-level configurations.
3871+
3872+
[source,java,indent=0]
3873+
----
3874+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_class_level]
3875+
----
3876+
3877+
NOTE: A class-level configuration means that the specified locale is set before and reset
3878+
after each individual test in the annotated class.
3879+
3880+
If your use case is not covered, you can implement the `{LocaleProvider}` interface.
3881+
3882+
[source,java,indent=0]
3883+
----
3884+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_with_provider]
3885+
----
3886+
3887+
NOTE: The provider implementation must have a no-args (or the default) constructor.
3888+
3889+
[[writing-tests-built-in-extensions-DefaultTimeZone]]
3890+
===== @DefaultTimeZone
3891+
3892+
The default `TimeZone` is specified according to the
3893+
{jdk-javadoc-base-url}/java.base/java/util/TimeZone.html#getTimeZone(java.lang.String)[TimeZone.getTimeZone(String)]
3894+
method.
3895+
3896+
[source,java,indent=0]
3897+
----
3898+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_zone]
3899+
----
3900+
3901+
Any method level `@DefaultTimeZone` configurations will override class level configurations:
3902+
3903+
[source,java,indent=0]
3904+
----
3905+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_class_level]
3906+
----
3907+
3908+
NOTE: A class-level configuration means that the specified time zone is set before and
3909+
reset after each individual test in the annotated class.
3910+
3911+
If your use case is not covered, you can implement the `{TimeZoneProvider}` interface.
3912+
3913+
[source,java,indent=0]
3914+
----
3915+
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_time_zone_with_provider]
3916+
----
3917+
3918+
NOTE: The provider implementation must have a no-args (or the default) constructor.
3919+
3920+
===== Thread Safety
3921+
3922+
Since the default locale and time zone are global state, reading and writing them during
3923+
<<writing-tests-parallel-execution, parallel test execution>> can lead to unpredictable
3924+
results and flaky tests. The `@DefaultLocale` and `@DefaultTimeZone` extensions are
3925+
prepared for that and tests annotated with them will never execute in parallel (thanks to
3926+
`{ResourceLock}`) to guarantee correct test results.
3927+
3928+
However, this does not cover all possible cases. Tested code that reads or writes default
3929+
locale and time zone _independently_ of the extensions can still run in parallel to them
3930+
and may thus behave erratically when, for example, it unexpectedly reads a locale set by
3931+
the extension in another thread. Tests that cover code that reads or writes the default
3932+
locale or time zone need to be annotated with the respective annotation:
3933+
3934+
* `{ReadsDefaultLocale}`
3935+
* `{ReadsDefaultTimeZone}`
3936+
* `{WritesDefaultLocale}`
3937+
* `{WritesDefaultTimeZone}`
3938+
3939+
Tests annotated in this way will never execute in parallel with tests annotated with
3940+
`@DefaultLocale` or `@DefaultTimeZone`.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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.util.DefaultLocale;
22+
import org.junit.jupiter.api.util.DefaultTimeZone;
23+
import org.junit.jupiter.api.util.LocaleProvider;
24+
import org.junit.jupiter.api.util.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+
@Override
85+
public Locale get() {
86+
return Locale.ENGLISH;
87+
}
88+
}
89+
// end::default_locale_with_provider[]
90+
91+
// tag::default_timezone_zone[]
92+
@Test
93+
@DefaultTimeZone("CET")
94+
void test_with_short_zone_id() {
95+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
96+
}
97+
98+
@Test
99+
@DefaultTimeZone("Africa/Juba")
100+
void test_with_long_zone_id() {
101+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
102+
}
103+
// end::default_timezone_zone[]
104+
105+
@Nested
106+
// tag::default_timezone_class_level[]
107+
@DefaultTimeZone("CET")
108+
class MyTimeZoneTests {
109+
110+
@Test
111+
void test_with_class_level_configuration() {
112+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
113+
}
114+
115+
@Test
116+
@DefaultTimeZone("Africa/Juba")
117+
void test_with_method_level_configuration() {
118+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
119+
}
120+
121+
}
122+
// end::default_timezone_class_level[]
123+
124+
// tag::default_time_zone_with_provider[]
125+
@Test
126+
@DefaultTimeZone(timeZoneProvider = UtcTimeZoneProvider.class)
127+
void test_with_time_zone_provider() {
128+
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("UTC"));
129+
}
130+
131+
static class UtcTimeZoneProvider implements TimeZoneProvider {
132+
@Override
133+
public TimeZone get() {
134+
return TimeZone.getTimeZone(ZoneOffset.UTC);
135+
}
136+
}
137+
// end::default_time_zone_with_provider[]
138+
139+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
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.util;
3435

3536
opens org.junit.jupiter.api.condition to org.junit.platform.commons;
37+
opens org.junit.jupiter.api.util to org.junit.platform.commons;
3638
}

0 commit comments

Comments
 (0)