Skip to content
Draft
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
98 changes: 98 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3826,3 +3826,101 @@ 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

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

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.

===== `@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 of which a {jdk-javadoc-base-url}/java.base/java/util/Locale.Builder.html[Locale Builder] can create an instance with:

* `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]
----

Note that mixing language tag configuration and constructor based configuration will cause an `ExtensionConfigurationException` to be thrown.
Furthermore, a `variant` can only be specified if `country` is also specified.
If `variant` is specified without `country`, an `ExtensionConfigurationException` 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.

===== `@DefaultTimeZone`

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.

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

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,143 @@
/*
* 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[]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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 org.junit.jupiter.api.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.apiguardian.api.API;

/**
* {@code @DefaultLocale} is a JUnit Jupiter extension to change the value
* returned by {@link java.util.Locale#getDefault()} for a test execution.
*
* <p>The {@link java.util.Locale} to set as the default locale can be
* configured in several ways:</p>
*
* <ul>
* <li>using a {@link java.util.Locale#forLanguageTag(String) language tag}</li>
* <li>using a {@link java.util.Locale.Builder Locale.Builder} together with
* <ul>
* <li>a language</li>
* <li>a language and a county</li>
* <li>a language, a county, and a variant</li>
* </ul>
* </li>
* </ul>
*
* <p>Please keep in mind that the {@code Locale.Builder} does a syntax check, if you use a variant!
* 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>
*
* <p>If a language tag is set, none of the other fields must be set. Otherwise, an
* {@link org.junit.jupiter.api.extension.ExtensionConfigurationException} will
* be thrown. Specifying a {@link #country()} but no {@link #language()}, or a
* {@link #variant()} but no {@link #country()} and {@link #language()} will
* also cause an {@code ExtensionConfigurationException}. After the annotated
* element has been executed, the default {@code Locale} will be restored to
* its original value.</p>
*
* <p>{@code @DefaultLocale} can be used on the method and on the class level. It
* is inherited from higher-level containers, but can only be used once per method
* or class. If a class is annotated, the configured {@code Locale} will be the
* default {@code Locale} for all tests inside that class. Any method level
* configurations will override the class level default {@code Locale}.</p>
*
* <p>During
* <a href="https://docs.junit.org/current/user-guide/#writing-tests-parallel-execution" target="_top">parallel test execution</a>,
* all tests annotated with {@link DefaultLocale}, {@link ReadsDefaultLocale}, and {@link WritesDefaultLocale}
* are scheduled in a way that guarantees correctness under mutation of shared global state.</p>
*
* <p>For more details and examples, see
* <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>
*
* @since 6.1
* @see java.util.Locale#getDefault()
* @see DefaultTimeZone
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
@Inherited
@WritesDefaultLocale
@API(status = API.Status.STABLE, since = "6.1")
public @interface DefaultLocale {

/**
* A language tag string as specified by IETF BCP 47. See
* {@link java.util.Locale#forLanguageTag(String)} for more information
* about valid language tag values.
*
* @since 0.3
*/
String value() default "";

/**
* An ISO 639 alpha-2 or alpha-3 language code, or a language subtag up to
* 8 characters in length. See the {@link java.util.Locale} class
* description about valid language values.
*/
String language() default "";

/**
* An ISO 3166 alpha-2 country code or a UN M.49 numeric-3 area code. See
* the {@link java.util.Locale} class description about valid country
* values.
*/
String country() default "";

/**
* An IETF BCP 47 language string that matches the <a href="https://www.rfc-editor.org/rfc/rfc5646.html">RFC 5646</a> syntax.
* It's validated by the {@code Locale.Builder}, using {@code sun.util.locale.LanguageTag#isVariant}.
*/
String variant() default "";

/**
* A class implementing {@link LocaleProvider} to be used for custom {@code Locale} resolution.
* This is mutually exclusive with other properties, if any other property is given a value it
* will result in an {@link org.junit.jupiter.api.extension.ExtensionConfigurationException}.
*/
Class<? extends LocaleProvider> localeProvider() default LocaleProvider.NullLocaleProvider.class;

}
Loading
Loading