Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ endif::[]
:TestMethodOrder: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/TestMethodOrder.html[@TestMethodOrder]
:TestReporter: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/TestReporter.html[TestReporter]
:TestTemplate: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/TestTemplate.html[@TestTemplate]
// @DefaultLocale and @DefaultTimeZone
:DefaultLocale: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/DefaultLocale.html[@DefaultLocale]
:DefaultTimeZone: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/DefaultTimeZone.html[@DefaultTimeZone]
:LocaleProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/LocaleProvider.html[LocaleProvider]
:TimeZoneProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/TimeZoneProvider.html[TimeZoneProvider]
:ReadsDefaultLocale: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/ReadsDefaultLocale.html[@ReadsDefaultLocale]
:ReadsDefaultTimeZone: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/ReadsDefaultTimeZone.html[@ReadsDefaultTimeZone]
:WritesDefaultLocale: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/WritesDefaultLocale.html[@WritesDefaultLocale]
:WritesDefaultTimeZone: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/util/WritesDefaultTimeZone.html[@WritesDefaultTimeZone]
// Jupiter Parallel API
:Execution: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Execution.html[@Execution]
:Isolated: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Isolated.html[@Isolated]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ For a complete list of all _closed_ issues and pull requests for this release, c
link:{junit-framework-repo}+/milestone/112?closed=1+[6.1.0-M2] milestone page in the JUnit
repository on GitHub.


[[release-notes-6.1.0-M2-junit-platform]]
=== JUnit Platform

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

* ❓


[[release-notes-6.1.0-M2-junit-jupiter]]
=== JUnit Jupiter

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

* `JAVA_27` has been added to the `JRE` enum for use with `JRE`-based execution conditions.

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

[[release-notes-6.1.0-M2-junit-vintage]]
=== JUnit Vintage
Expand Down
114 changes: 114 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3824,3 +3824,117 @@ include::{testDir}/example/AutoCloseDemo.java[tags=user_guide_example]
<1> Annotate an instance field with `@AutoClose`.
<2> `WebClient` implements `java.lang.AutoCloseable` which defines a `close()` method that
will be invoked after each `@Test` method.

[[writing-tests-built-in-extensions-DefaultLocaleAndTimeZone]]
==== The @DefaultLocale and @DefaultTimeZone Extensions

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. Both annotations
work on the test class level and on the test method level, and are inherited from
higher-level containers. After the annotated element has been executed, the initial
default value is restored.

[[writing-tests-built-in-extensions-DefaultLocale]]
===== @DefaultLocale

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].

[source,java,indent=0]
----
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tags=default_locale_language]
----

Alternatively, the default `Locale` can be created using the following attributes from
which a {jdk-javadoc-base-url}/java.base/java/util/Locale.Builder.html[`Locale.Builder`]
can create an instance:

* `language` or
* `language` and `country` or
* `language`, `country`, and `variant`

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

[source,java,indent=0]
----
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_language_alternatives]
----

Mixing language tag configuration (via the annotation's `value` attributed) and
attributed-based configuration will cause an exception to be thrown. Furthermore, a
`variant` can only be specified if `country` is also specified. Otherwise, an exception
will be thrown.

Any method-level `@DefaultLocale` configurations will override class-level configurations.

[source,java,indent=0]
----
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_class_level]
----

NOTE: A class-level configuration means that the specified locale is set before and reset
after each individual test in the annotated class.

If your use case is not covered, you can implement the `{LocaleProvider}` interface.

[source,java,indent=0]
----
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_locale_with_provider]
----

NOTE: The provider implementation must have a no-args (or the default) constructor.

[[writing-tests-built-in-extensions-DefaultTimeZone]]
===== @DefaultTimeZone

The default `TimeZone` is specified according to the
{jdk-javadoc-base-url}/java.base/java/util/TimeZone.html#getTimeZone(java.lang.String)[TimeZone.getTimeZone(String)]
method.

[source,java,indent=0]
----
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_zone]
----

Any method level `@DefaultTimeZone` configurations will override class level configurations:

[source,java,indent=0]
----
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_timezone_class_level]
----

NOTE: A class-level configuration means that the specified time zone is set before and
reset after each individual test in the annotated class.

If your use case is not covered, you can implement the `{TimeZoneProvider}` interface.

[source,java,indent=0]
----
include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tag=default_time_zone_with_provider]
----

NOTE: The provider implementation must have a no-args (or the default) constructor.

===== Thread Safety

Since the default locale and time zone are global state, reading and writing them during
<<writing-tests-parallel-execution, parallel test execution>> can lead to unpredictable
results and flaky tests. The `@DefaultLocale` and `@DefaultTimeZone` extensions are
prepared for that and tests annotated with them will never execute in parallel (thanks to
`{ResourceLock}`) to guarantee correct test results.

However, this does not cover all possible cases. 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. Tests that cover code that reads or writes the default
locale or time zone need to be annotated with the respective annotation:

* `{ReadsDefaultLocale}`
* `{ReadsDefaultTimeZone}`
* `{WritesDefaultLocale}`
* `{WritesDefaultTimeZone}`

Tests annotated in this way will never execute in parallel with tests annotated with
`@DefaultLocale` or `@DefaultTimeZone`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.ZoneOffset;
import java.util.Locale;
import java.util.TimeZone;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.util.DefaultLocale;
import org.junit.jupiter.api.util.DefaultTimeZone;
import org.junit.jupiter.api.util.LocaleProvider;
import org.junit.jupiter.api.util.TimeZoneProvider;

public class DefaultLocaleTimezoneExtensionDemo {

// tag::default_locale_language[]
@Test
@DefaultLocale("zh-Hant-TW")
void test_with_language() {
assertThat(Locale.getDefault()).isEqualTo(Locale.forLanguageTag("zh-Hant-TW"));
}
// end::default_locale_language[]

// tag::default_locale_language_alternatives[]
@Test
@DefaultLocale(language = "en")
void test_with_language_only() {
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
}

@Test
@DefaultLocale(language = "en", country = "EN")
void test_with_language_and_country() {
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").setRegion("EN").build());
}

@Test
@DefaultLocale(language = "ja", country = "JP", variant = "japanese")
void test_with_language_and_country_and_vairant() {
assertThat(Locale.getDefault()).isEqualTo(
new Locale.Builder().setLanguage("ja").setRegion("JP").setVariant("japanese").build());
}
// end::default_locale_language_alternatives[]

@Nested
// tag::default_locale_class_level[]
@DefaultLocale(language = "fr")
class MyLocaleTests {

@Test
void test_with_class_level_configuration() {
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("fr").build());
}

@Test
@DefaultLocale(language = "en")
void test_with_method_level_configuration() {
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
}

}
// end::default_locale_class_level[]

// tag::default_locale_with_provider[]
@Test
@DefaultLocale(localeProvider = EnglishProvider.class)
void test_with_locale_provider() {
assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
}

static class EnglishProvider implements LocaleProvider {
@Override
public Locale get() {
return Locale.ENGLISH;
}
}
// end::default_locale_with_provider[]

// tag::default_timezone_zone[]
@Test
@DefaultTimeZone("CET")
void test_with_short_zone_id() {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
}

@Test
@DefaultTimeZone("Africa/Juba")
void test_with_long_zone_id() {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
}
// end::default_timezone_zone[]

@Nested
// tag::default_timezone_class_level[]
@DefaultTimeZone("CET")
class MyTimeZoneTests {

@Test
void test_with_class_level_configuration() {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
}

@Test
@DefaultTimeZone("Africa/Juba")
void test_with_method_level_configuration() {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
}

}
// end::default_timezone_class_level[]

// tag::default_time_zone_with_provider[]
@Test
@DefaultTimeZone(timeZoneProvider = UtcTimeZoneProvider.class)
void test_with_time_zone_provider() {
assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("UTC"));
}

static class UtcTimeZoneProvider implements TimeZoneProvider {
@Override
public TimeZone get() {
return TimeZone.getTimeZone(ZoneOffset.UTC);
}
}
// end::default_time_zone_with_provider[]

}
2 changes: 2 additions & 0 deletions junit-jupiter-api/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
exports org.junit.jupiter.api.io;
exports org.junit.jupiter.api.parallel;
exports org.junit.jupiter.api.timeout to org.junit.jupiter.engine;
exports org.junit.jupiter.api.util;

opens org.junit.jupiter.api.condition to org.junit.platform.commons;
opens org.junit.jupiter.api.util to org.junit.platform.commons;
}
Loading