From 2aed3ea8d12b52b69471ecc5bc080095297b92c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Fri, 7 Nov 2025 20:04:48 +0100 Subject: [PATCH 1/7] Initial draft for providing DefaultLocale and DefaulTimezone extensions --- .../asciidoc/user-guide/writing-tests.adoc | 98 +++ .../DefaultLocaleTimezoneExtensionDemo.java | 143 ++++ .../junit/jupiter/api/util/DefaultLocale.java | 108 +++ .../jupiter/api/util/DefaultTimeZone.java | 69 ++ .../jupiter/api/util/LocaleProvider.java | 21 + .../jupiter/api/util/ReadsDefaultLocale.java | 41 + .../api/util/ReadsDefaultTimeZone.java | 41 + .../jupiter/api/util/TimeZoneProvider.java | 21 + .../jupiter/api/util/WritesDefaultLocale.java | 41 + .../api/util/WritesDefaultTimeZone.java | 41 + .../extension/DefaultLocaleExtension.java | 130 +++ .../extension/DefaultTimeZoneExtension.java | 106 +++ .../extension/MutableExtensionRegistry.java | 2 + .../engine/support/JupiterLocaleUtils.java | 36 + .../engine/extension/DefaultLocaleTests.java | 515 ++++++++++++ .../extension/DefaultTimeZoneTests.java | 378 +++++++++ .../jupiter/testkit/ExecutionResults.java | 152 ++++ .../jupiter/testkit/JUnitJupiterKitTests.java | 120 +++ .../jupiter/testkit/JUnitJupiterTestKit.java | 163 ++++ .../assertion/AbstractJUnitJupiterAssert.java | 38 + .../assertion/ExecutionResultAssert.java | 21 + .../testkit/assertion/JUnitJupiterAssert.java | 45 ++ .../JUnitJupiterExecutionResultAssert.java | 317 ++++++++ .../assertion/JUnitJupiterPathAssert.java | 64 ++ .../testkit/assertion/PropertiesAssert.java | 194 +++++ .../assertion/PropertiesAssertTests.java | 749 ++++++++++++++++++ .../testkit/assertion/TestCaseAssertBase.java | 86 ++ .../single/TestCaseAbortedAssert.java | 28 + .../assertion/single/TestCaseAssert.java | 90 +++ .../single/TestCaseFailureAssert.java | 52 ++ .../single/TestCaseStartedAssert.java | 38 + .../assertion/suite/TestSuiteAssert.java | 17 + .../suite/TestSuiteContainersAssert.java | 66 ++ .../suite/TestSuiteFailureAssert.java | 55 ++ .../assertion/suite/TestSuiteTestsAssert.java | 66 ++ 35 files changed, 4152 insertions(+) create mode 100644 documentation/src/test/java/example/DefaultLocaleTimezoneExtensionDemo.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultLocaleExtension.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/ExecutionResults.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterTestKit.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index d3ccc0e96312..026beff1e3d9 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -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-DefaultLocalAndTimezone]] +==== The @DefaultLocal 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 https://docs.oracle.com/javase/8/docs/api/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 https://docs.oracle.com/javase/8/docs/api/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`. diff --git a/documentation/src/test/java/example/DefaultLocaleTimezoneExtensionDemo.java b/documentation/src/test/java/example/DefaultLocaleTimezoneExtensionDemo.java new file mode 100644 index 000000000000..75a9281c6bf3 --- /dev/null +++ b/documentation/src/test/java/example/DefaultLocaleTimezoneExtensionDemo.java @@ -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[] + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java new file mode 100644 index 000000000000..9cc73236b95c --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java @@ -0,0 +1,108 @@ +/* + * 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; + +/** + * {@code @DefaultLocale} is a JUnit Jupiter extension to change the value + * returned by {@link java.util.Locale#getDefault()} for a test execution. + * + *

The {@link java.util.Locale} to set as the default locale can be + * configured in several ways:

+ * + * + * + *

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 RFC 5646) syntax.

+ * + *

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.

+ * + *

{@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}.

+ * + *

During + * parallel test execution, + * 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.

+ * + *

For more details and examples, see + * the documentation on @DefaultLocale and @DefaultTimeZone.

+ * + * @since 6.1 + * @see java.util.Locale#getDefault() + * @see DefaultTimeZone + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Inherited +@WritesDefaultLocale +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 RFC 5646 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 localeProvider() default LocaleProvider.NullLocaleProvider.class; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java new file mode 100644 index 000000000000..517598d1c779 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java @@ -0,0 +1,69 @@ +/* + * 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; + +/** + * {@code @DefaultTimeZone} is a JUnit Jupiter extension to change the value + * returned by {@link java.util.TimeZone#getDefault()} for a test execution. + * + *

The {@link java.util.TimeZone} to set as the default {@code TimeZone} is + * configured by specifying the {@code TimeZone} ID as defined by + * {@link java.util.TimeZone#getTimeZone(String)}. After the annotated element + * has been executed, the default {@code TimeZone} will be restored to its + * original value.

+ * + *

{@code @DefaultTimeZone} 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 TimeZone} will be the default {@code TimeZone} for all tests inside + * that class. Any method level configurations will override the class level + * default {@code TimeZone}.

+ * + *

During + * parallel test execution, + * all tests annotated with {@link DefaultTimeZone}, {@link ReadsDefaultTimeZone}, and {@link WritesDefaultTimeZone} + * are scheduled in a way that guarantees correctness under mutation of shared global state.

+ * + *

For more details and examples, see + * the documentation on @DefaultLocale and @DefaultTimeZone.

+ * + * @since 6.1 + * @see java.util.TimeZone#getDefault() + * @see DefaultLocale + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Inherited +@WritesDefaultTimeZone +public @interface DefaultTimeZone { + + /** + * The ID for a {@code TimeZone}, either an abbreviation such as "PST", a + * full name such as "America/Los_Angeles", or a custom ID such as + * "GMT-8:00". Note that the support of abbreviations is for JDK 1.1.x + * compatibility only and full names should be used. + */ + String value() default ""; + + /** + * A class implementing {@link TimeZoneProvider} to be used for custom {@code TimeZone} 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 timeZoneProvider() default TimeZoneProvider.NullTimeZoneProvider.class; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java new file mode 100644 index 000000000000..d598385e9158 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java @@ -0,0 +1,21 @@ +/* + * 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.util.Locale; +import java.util.function.Supplier; + +public interface LocaleProvider extends Supplier { + + interface NullLocaleProvider extends LocaleProvider { + } + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java new file mode 100644 index 000000000000..9d60abefd920 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java @@ -0,0 +1,41 @@ +/* + * 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.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; + +/** + * Marks tests that read the default locale but don't use the locale extension themselves. + * + *

During + * parallel test execution, + * 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.

+ * + *

For more details and examples, see + * the documentation on @DefaultLocale and @DefaultTimeZone.

+ * + * @since 6.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) +@Inherited +@ResourceLock(value = Resources.LOCALE, mode = ResourceAccessMode.READ) +public @interface ReadsDefaultLocale { +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java new file mode 100644 index 000000000000..de6f2642c3a8 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java @@ -0,0 +1,41 @@ +/* + * 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.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; + +/** + * Marks tests that read the default time zone but don't use the time zone extension themselves. + * + *

During + * parallel test execution, + * all tests annotated with {@link DefaultTimeZone}, {@link ReadsDefaultTimeZone}, and {@link WritesDefaultTimeZone} + * are scheduled in a way that guarantees correctness under mutation of shared global state.

+ * + *

For more details and examples, see + * the documentation on @DefaultLocale and @DefaultTimeZone.

+ * + * @since 6.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) +@Inherited +@ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ) +public @interface ReadsDefaultTimeZone { +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java new file mode 100644 index 000000000000..ed180dbcea75 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java @@ -0,0 +1,21 @@ +/* + * 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.util.TimeZone; +import java.util.function.Supplier; + +public interface TimeZoneProvider extends Supplier { + + interface NullTimeZoneProvider extends TimeZoneProvider { + } + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java new file mode 100644 index 000000000000..ca0a1dc15020 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java @@ -0,0 +1,41 @@ +/* + * 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.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; + +/** + * Marks tests that write the default locale but don't use the locale extension themselves. + * + *

During + * parallel test execution, + * 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.

+ * + *

For more details and examples, see + * the documentation on @DefaultLocale and @DefaultTimeZone.

+ * + * @since 6.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) +@Inherited +@ResourceLock(value = Resources.LOCALE, mode = ResourceAccessMode.READ_WRITE) +public @interface WritesDefaultLocale { +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java new file mode 100644 index 000000000000..917ff1541d9b --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java @@ -0,0 +1,41 @@ +/* + * 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.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; + +/** + * Marks tests that write the default time zone but don't use the time zone extension themselves. + * + *

During + * parallel test execution, + * all tests annotated with {@link DefaultTimeZone}, {@link ReadsDefaultTimeZone}, and {@link WritesDefaultTimeZone} + * are scheduled in a way that guarantees correctness under mutation of shared global state.

+ * + *

For more details and examples, see + * the documentation on @DefaultLocale and @DefaultTimeZone.

+ * + * @since 6.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) +@Inherited +@ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ_WRITE) +public @interface WritesDefaultTimeZone { +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultLocaleExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultLocaleExtension.java new file mode 100644 index 000000000000..02ebf0086115 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultLocaleExtension.java @@ -0,0 +1,130 @@ +/* + * 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.engine.extension; + +import java.lang.reflect.AnnotatedElement; +import java.util.Locale; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionConfigurationException; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.util.DefaultLocale; +import org.junit.jupiter.api.util.LocaleProvider; +import org.junit.jupiter.api.util.LocaleProvider.NullLocaleProvider; +import org.junit.jupiter.engine.support.JupiterLocaleUtils; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.support.ReflectionSupport; + +class DefaultLocaleExtension implements BeforeEachCallback, AfterEachCallback { + + private static final Namespace NAMESPACE = Namespace.create(DefaultLocaleExtension.class); + + private static final String KEY = "DefaultLocale"; + + @Override + public void beforeEach(ExtensionContext context) { + AnnotatedElement element = context.getElement().orElse(null); + AnnotationSupport.findAnnotation(element, DefaultLocale.class).ifPresent( + annotation -> setDefaultLocale(context, annotation)); + } + + private void setDefaultLocale(ExtensionContext context, DefaultLocale annotation) { + Locale configuredLocale = createLocale(annotation); + // defer storing the current default locale until the new locale could be created from the configuration + // (this prevents cases where misconfigured extensions store default locale now and restore it later, + // which leads to race conditions in our tests) + storeDefaultLocale(context); + Locale.setDefault(configuredLocale); + } + + private void storeDefaultLocale(ExtensionContext context) { + context.getStore(NAMESPACE).put(KEY, Locale.getDefault()); + } + + private static Locale createLocale(DefaultLocale annotation) { + if (!annotation.value().isEmpty()) { + return createFromLanguageTag(annotation); + } + else if (!annotation.language().isEmpty()) { + return createFromParts(annotation); + } + else { + return getFromProvider(annotation); + } + } + + private static Locale createFromLanguageTag(DefaultLocale annotation) { + if (!annotation.language().isEmpty() || !annotation.country().isEmpty() || !annotation.variant().isEmpty() + || annotation.localeProvider() != NullLocaleProvider.class) { + throw new ExtensionConfigurationException( + "@DefaultLocale can only be used with language tag if language, country, variant and provider are not set"); + } + return Locale.forLanguageTag(annotation.value()); + } + + private static Locale createFromParts(DefaultLocale annotation) { + if (annotation.localeProvider() != NullLocaleProvider.class) + throw new ExtensionConfigurationException( + "@DefaultLocale can only be used with language tag if provider is not set"); + String language = annotation.language(); + String country = annotation.country(); + String variant = annotation.variant(); + if (!language.isEmpty() && !country.isEmpty() && !variant.isEmpty()) { + return JupiterLocaleUtils.createLocale(language, country, variant); + } + else if (!language.isEmpty() && !country.isEmpty()) { + return JupiterLocaleUtils.createLocale(language, country); + } + else if (!language.isEmpty() && variant.isEmpty()) { + return JupiterLocaleUtils.createLocale(language); + } + else { + throw new ExtensionConfigurationException( + "@DefaultLocale not configured correctly. When not using a language tag, specify either" + + " language, or language and country, or language and country and variant."); + } + } + + private static Locale getFromProvider(DefaultLocale annotation) { + if (!annotation.country().isEmpty() || !annotation.variant().isEmpty()) + throw new ExtensionConfigurationException( + "@DefaultLocale can only be used with a provider if value, language, country and variant are not set."); + var providerClass = annotation.localeProvider(); + LocaleProvider provider; + try { + provider = ReflectionSupport.newInstance(providerClass); + } + catch (Exception exception) { + throw new ExtensionConfigurationException( + "LocaleProvider instance could not be constructed because of an exception", exception); + } + var locale = provider.get(); + if (locale == null) + throw new NullPointerException("LocaleProvider instance returned with null"); + return locale; + } + + @Override + public void afterEach(ExtensionContext context) { + AnnotatedElement element = context.getElement().orElse(null); + AnnotationSupport.findAnnotation(element, DefaultLocale.class).ifPresent(__ -> resetDefaultLocale(context)); + } + + private void resetDefaultLocale(ExtensionContext context) { + Locale defaultLocale = context.getStore(NAMESPACE).get(KEY, Locale.class); + // default locale is null if the extension was misconfigured and execution failed in "before" + if (defaultLocale != null) + Locale.setDefault(defaultLocale); + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java new file mode 100644 index 000000000000..004887e1f841 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java @@ -0,0 +1,106 @@ +/* + * 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.engine.extension; + +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; +import java.util.TimeZone; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionConfigurationException; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.util.DefaultTimeZone; +import org.junit.jupiter.api.util.TimeZoneProvider; +import org.junit.jupiter.api.util.TimeZoneProvider.NullTimeZoneProvider; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.support.ReflectionSupport; + +class DefaultTimeZoneExtension implements BeforeEachCallback, AfterEachCallback { + + private static final Namespace NAMESPACE = Namespace.create(DefaultTimeZoneExtension.class); + + private static final String KEY = "DefaultTimeZone"; + + @Override + public void beforeEach(ExtensionContext context) { + AnnotatedElement element = context.getElement().orElse(null); + AnnotationSupport.findAnnotation(element, DefaultTimeZone.class).ifPresent( + annotation -> setDefaultTimeZone(context.getStore(NAMESPACE), annotation)); + } + + private void setDefaultTimeZone(Store store, DefaultTimeZone annotation) { + validateCorrectConfiguration(annotation); + TimeZone defaultTimeZone; + if (annotation.timeZoneProvider() != NullTimeZoneProvider.class) + defaultTimeZone = createTimeZone(annotation.timeZoneProvider()); + else + defaultTimeZone = createTimeZone(annotation.value()); + // defer storing the current default time zone until the new time zone could be created from the configuration + // (this prevents cases where misconfigured extensions store default time zone now and restore it later, + // which leads to race conditions in our tests) + storeDefaultTimeZone(store); + TimeZone.setDefault(defaultTimeZone); + } + + private static void validateCorrectConfiguration(DefaultTimeZone annotation) { + boolean noValue = annotation.value().isEmpty(); + boolean noProvider = annotation.timeZoneProvider() == NullTimeZoneProvider.class; + if (noValue == noProvider) + throw new ExtensionConfigurationException( + "Either a valid time zone id or a TimeZoneProvider must be provided to " + + DefaultTimeZone.class.getSimpleName()); + } + + private static TimeZone createTimeZone(String timeZoneId) { + TimeZone configuredTimeZone = TimeZone.getTimeZone(timeZoneId); + // TimeZone::getTimeZone returns with GMT as fallback if the given ID cannot be understood + if (configuredTimeZone.equals(TimeZone.getTimeZone("GMT")) && !timeZoneId.equals("GMT")) { + throw new ExtensionConfigurationException(String.format( + "@DefaultTimeZone not configured correctly. " + "Could not find the specified time zone + '%s'. " + + "Please use correct identifiers, e.g. \"GMT\" for Greenwich Mean Time.", + timeZoneId)); + } + return configuredTimeZone; + } + + private static TimeZone createTimeZone(Class providerClass) { + try { + TimeZoneProvider provider = ReflectionSupport.newInstance(providerClass); + return Optional.ofNullable(provider.get()).orElse(TimeZone.getTimeZone("GMT")); + } + catch (Exception exception) { + throw new ExtensionConfigurationException("Could not instantiate TimeZoneProvider because of exception", + exception); + } + } + + private void storeDefaultTimeZone(Store store) { + store.put(KEY, TimeZone.getDefault()); + } + + @Override + public void afterEach(ExtensionContext context) { + AnnotatedElement element = context.getElement().orElse(null); + AnnotationSupport.findAnnotation(element, DefaultTimeZone.class).ifPresent( + __ -> resetDefaultTimeZone(context.getStore(NAMESPACE))); + } + + private void resetDefaultTimeZone(Store store) { + TimeZone timeZone = store.get(KEY, TimeZone.class); + // default time zone is null if the extension was misconfigured and execution failed in "before" + if (timeZone != null) + TimeZone.setDefault(timeZone); + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java index e5037f938262..129384af63dc 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java @@ -83,6 +83,8 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit DEFAULT_STATELESS_EXTENSIONS.forEach(extensionRegistry::registerDefaultExtension); extensionRegistry.registerDefaultExtension(new TempDirectory(configuration)); + extensionRegistry.registerDefaultExtension(new DefaultLocaleExtension()); + extensionRegistry.registerDefaultExtension(new DefaultTimeZoneExtension()); if (configuration.isExtensionAutoDetectionEnabled()) { registerAutoDetectedExtensions(extensionRegistry, configuration); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java new file mode 100644 index 000000000000..6b651ed43799 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java @@ -0,0 +1,36 @@ +/* + * 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.engine.support; + +import java.util.Locale; + +/** + * Utility class to create {@code Locale}. + */ +public class JupiterLocaleUtils { + + private JupiterLocaleUtils() { + // private constructor to prevent instantiation of utility class + } + + public static Locale createLocale(String language, String country, String variant) { + return new Locale.Builder().setLanguage(language).setRegion(country).setVariant(variant).build(); + } + + public static Locale createLocale(String language, String country) { + return new Locale.Builder().setLanguage(language).setRegion(country).build(); + } + + public static Locale createLocale(String language) { + return new Locale.Builder().setLanguage(language).build(); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java new file mode 100644 index 000000000000..ca264ee1161b --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java @@ -0,0 +1,515 @@ +/* + * 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.engine.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestClass; +import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestMethod; +import static org.junit.jupiter.testkit.assertion.JUnitJupiterAssert.assertThat; + +import java.util.Locale; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtensionConfigurationException; +import org.junit.jupiter.api.util.DefaultLocale; +import org.junit.jupiter.api.util.LocaleProvider; +import org.junit.jupiter.api.util.ReadsDefaultLocale; +import org.junit.jupiter.api.util.WritesDefaultLocale; +import org.junit.jupiter.engine.support.JupiterLocaleUtils; +import org.junit.jupiter.testkit.ExecutionResults; + +@DisplayName("DefaultLocale extension") +class DefaultLocaleTests { + + private static Locale TEST_DEFAULT_LOCALE; + private static Locale DEFAULT_LOCALE_BEFORE_TEST; + + @BeforeAll + static void globalSetUp() { + DEFAULT_LOCALE_BEFORE_TEST = Locale.getDefault(); + TEST_DEFAULT_LOCALE = JupiterLocaleUtils.createLocale("custom"); + Locale.setDefault(TEST_DEFAULT_LOCALE); + } + + @AfterAll + static void globalTearDown() { + Locale.setDefault(DEFAULT_LOCALE_BEFORE_TEST); + } + + @Nested + @DisplayName("applied on the method level") + class MethodLevelTests { + + @Test + @ReadsDefaultLocale + @DisplayName("does nothing when annotation is not present") + void testDefaultLocaleNoAnnotation() { + assertThat(Locale.getDefault()).isEqualTo(TEST_DEFAULT_LOCALE); + } + + @Test + @DefaultLocale("zh-Hant-TW") + @DisplayName("sets the default locale using a language tag") + void setsLocaleViaLanguageTag() { + assertThat(Locale.getDefault()).isEqualTo(Locale.forLanguageTag("zh-Hant-TW")); + } + + @Test + @DefaultLocale(language = "en") + @DisplayName("sets the default locale using a language") + void setsLanguage() { + assertThat(Locale.getDefault()).isEqualTo(JupiterLocaleUtils.createLocale("en")); + } + + @Test + @DefaultLocale(language = "en", country = "EN") + @DisplayName("sets the default locale using a language and a country") + void setsLanguageAndCountry() { + assertThat(Locale.getDefault()).isEqualTo(JupiterLocaleUtils.createLocale("en", "EN")); + } + + /** + * A valid variant checked by {@link sun.util.locale.LanguageTag#isVariant} against BCP 47 (or more detailed RFC 5646) matches either {@code [0-9a-Z]{5-8}} or {@code [0-9][0-9a-Z]{3}}. + * It does NOT check if such a variant exists in real. + *
+ * The Locale-Builder accepts valid variants, concatenated by minus or underscore (minus will be transformed by the builder). + * This means "en-EN" is a valid languageTag, but not a valid IETF BCP 47 variant subtag. + *
+ * This is very confusing as the official page for supported locales shows that japanese locales return {@code *} or {@code JP} as a variant. + * Even more confusing the enum values {@code Locale.JAPAN} and {@code Locale.JAPANESE} don't return a variant. + * + * @see RFC 5646 + */ + @Test + @DefaultLocale(language = "ja", country = "JP", variant = "japanese") + @DisplayName("sets the default locale using a language, a country and a variant") + void setsLanguageAndCountryAndVariant() { + assertThat(Locale.getDefault()).isEqualTo(JupiterLocaleUtils.createLocale("ja", "JP", "japanese")); + } + + } + + @Test + @WritesDefaultLocale + @DisplayName("applied on the class level, should execute tests with configured Locale") + void shouldExecuteTestsWithConfiguredLocale() { + ExecutionResults results = executeTestClass(ClassLevelTestCases.class); + + assertThat(results).hasNumberOfSucceededTests(2); + } + + @DefaultLocale(language = "fr", country = "FR") + static class ClassLevelTestCases { + + @Test + @ReadsDefaultLocale + void shouldExecuteWithClassLevelLocale() { + assertThat(Locale.getDefault()).isEqualTo(JupiterLocaleUtils.createLocale("fr", "FR")); + } + + @Test + @DefaultLocale(language = "de", country = "DE") + void shouldBeOverriddenWithMethodLevelLocale() { + assertThat(Locale.getDefault()).isEqualTo(JupiterLocaleUtils.createLocale("de", "DE")); + } + + } + + @Nested + @DefaultLocale(language = "en") + @DisplayName("with nested classes") + class NestedDefaultLocaleTests { + + @Nested + @DisplayName("without DefaultLocale annotation") + class NestedClass { + + @Test + @DisplayName("DefaultLocale should be set from enclosed class when it is not provided in nested") + void shouldSetLocaleFromEnclosedClass() { + assertThat(Locale.getDefault().getLanguage()).isEqualTo("en"); + } + + } + + @Nested + @DefaultLocale(language = "de") + @DisplayName("with DefaultLocale annotation") + class AnnotatedNestedClass { + + @Test + @DisplayName("DefaultLocale should be set from nested class when it is provided") + void shouldSetLocaleFromNestedClass() { + assertThat(Locale.getDefault().getLanguage()).isEqualTo("de"); + } + + @Test + @DefaultLocale(language = "ch") + @DisplayName("DefaultLocale should be set from method when it is provided") + void shouldSetLocaleFromMethodOfNestedClass() { + assertThat(Locale.getDefault().getLanguage()).isEqualTo("ch"); + } + + } + + } + + @Nested + @DefaultLocale(language = "fi") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @DisplayName("correctly sets/resets before/after each/all extension points") + class ResettingDefaultLocaleTests { + + @Nested + @DefaultLocale(language = "de") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class ResettingDefaultLocaleNestedTests { + + @Test + @DefaultLocale(language = "en") + void setForTestMethod() { + // only here to set the locale, so another test can verify whether it was reset; + // still, better to assert the value was actually set + assertThat(Locale.getDefault().getLanguage()).isEqualTo("en"); + } + + @AfterAll + @ReadsDefaultLocale + void resetAfterTestMethodExecution() { + assertThat(Locale.getDefault().getLanguage()).isEqualTo("custom"); + } + + } + + @AfterAll + @ReadsDefaultLocale + void resetAfterTestMethodExecution() { + assertThat(Locale.getDefault().getLanguage()).isEqualTo("custom"); + } + + } + + @DefaultLocale(language = "en") + static class ClassLevelResetTestCase { + + @Test + void setForTestMethod() { + // only here to set the locale, so another test can verify whether it was reset; + // still, better to assert the value was actually set + assertThat(Locale.getDefault().getLanguage()).isEqualTo("en"); + } + + } + + @Nested + @DisplayName("when configured incorrect") + class ConfigurationFailureTests { + + @Nested + @DisplayName("on the method level") + class MethodLevel { + + @Test + @DisplayName("should fail when nothing is configured") + void shouldFailWhenNothingIsConfigured() { + ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, + "shouldFailMissingConfiguration"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class); + } + + @Test + @DisplayName("should fail when variant is set but country is not") + void shouldFailWhenVariantIsSetButCountryIsNot() { + ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, + "shouldFailMissingCountry"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class); + } + + @Test + @DisplayName("should fail when languageTag and language is set") + void shouldFailWhenLanguageTagAndLanguageIsSet() { + ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, + "shouldFailLanguageTagAndLanguage"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class); + } + + @Test + @DisplayName("should fail when languageTag and country is set") + void shouldFailWhenLanguageTagAndCountryIsSet() { + ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, + "shouldFailLanguageTagAndCountry"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class); + } + + @Test + @DisplayName("should fail when languageTag and variant is set") + void shouldFailWhenLanguageTagAndVariantIsSet() { + ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, + "shouldFailLanguageTagAndVariant"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class); + } + + @Test + @DisplayName("should fail when invalid BCP 47 variant is set") + void shouldFailIfNoValidBCP47VariantIsSet() { + ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, + "shouldFailNoValidBCP47Variant"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class); + } + + } + + @Nested + @DisplayName("on the class level") + class ClassLevel { + + @Test + @DisplayName("should fail when variant is set but country is not") + void shouldFailWhenVariantIsSetButCountryIsNot() { + ExecutionResults results = executeTestClass(ClassLevelInitializationFailureTestCases.class); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class); + } + + } + + } + + static class MethodLevelInitializationFailureTestCases { + + @Test + @DefaultLocale + void shouldFailMissingConfiguration() { + } + + @Test + @DefaultLocale(language = "de", variant = "ch") + void shouldFailMissingCountry() { + } + + @Test + @DefaultLocale(value = "Something", language = "de") + void shouldFailLanguageTagAndLanguage() { + } + + @Test + @DefaultLocale(value = "Something", country = "DE") + void shouldFailLanguageTagAndCountry() { + } + + @Test + @DefaultLocale(value = "Something", variant = "ch") + void shouldFailLanguageTagAndVariant() { + } + + @Test + @DefaultLocale(variant = "en-GB") + void shouldFailNoValidBCP47Variant() { + } + + } + + @DefaultLocale(language = "de", variant = "ch") + static class ClassLevelInitializationFailureTestCases { + + @Test + void shouldFail() { + } + + } + + @Nested + @DisplayName("used with inheritance") + class InheritanceTests extends InheritanceBaseTest { + + @Test + @DisplayName("should inherit default locale annotation") + void shouldInheritClearAndSetProperty() { + assertThat(Locale.getDefault()).isEqualTo(JupiterLocaleUtils.createLocale("fr", "FR")); + } + + } + + @DefaultLocale(language = "fr", country = "FR") + static class InheritanceBaseTest { + + } + + @Nested + @DisplayName("when used with a locale provider") + class LocaleProviderTests { + + @Test + @DisplayName("can get a basic locale from provider") + @DefaultLocale(localeProvider = BasicLocaleProvider.class) + void canUseProvider() { + assertThat(Locale.getDefault()).isEqualTo(Locale.FRENCH); + } + + @Test + @ReadsDefaultLocale + @DisplayName("throws a NullPointerException with custom message if provider returns null") + void providerReturnsNull() { + ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "returnsNull"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + NullPointerException.class).hasMessageContaining("LocaleProvider instance returned with null"); + } + + @Test + @ReadsDefaultLocale + @DisplayName("throws an ExtensionConfigurationException if any other option is present") + void mutuallyExclusiveWithValue() { + ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithValue"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "can only be used with language tag if language, country, variant and provider are not set"); + } + + @Test + @ReadsDefaultLocale + @DisplayName("throws an ExtensionConfigurationException if any other option is present") + void mutuallyExclusiveWithLanguage() { + ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithLanguage"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "can only be used with language tag if provider is not set"); + } + + @Test + @ReadsDefaultLocale + @DisplayName("throws an ExtensionConfigurationException if any other option is present") + void mutuallyExclusiveWithCountry() { + ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithCountry"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "can only be used with a provider if value, language, country and variant are not set."); + } + + @Test + @ReadsDefaultLocale + @DisplayName("throws an ExtensionConfigurationException if any other option is present") + void mutuallyExclusiveWithVariant() { + ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithVariant"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "can only be used with a provider if value, language, country and variant are not set."); + } + + @Test + @ReadsDefaultLocale + @DisplayName("throws an ExtensionConfigurationException if localeProvider can't be constructed") + void badConstructor() { + ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "badConstructor"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "could not be constructed because of an exception"); + } + + } + + static class BadProviderTestCases { + + @Test + @DefaultLocale(value = "en", localeProvider = BasicLocaleProvider.class) + void mutuallyExclusiveWithValue() { + // can't have both a value and a provider + } + + @Test + @DefaultLocale(language = "en", localeProvider = BasicLocaleProvider.class) + void mutuallyExclusiveWithLanguage() { + // can't have both a language property and a provider + } + + @Test + @DefaultLocale(country = "EN", localeProvider = BasicLocaleProvider.class) + void mutuallyExclusiveWithCountry() { + // can't have both a country property and a provider + } + + @Test + @DefaultLocale(variant = "japanese", localeProvider = BasicLocaleProvider.class) + void mutuallyExclusiveWithVariant() { + // can't have both a variant property and a provider + } + + @Test + @DefaultLocale(localeProvider = ReturnsNullLocaleProvider.class) + void returnsNull() { + // provider should not return 'null' + } + + @Test + @DefaultLocale(localeProvider = BadConstructorLocaleProvider.class) + void badConstructor() { + // provider has to have a no-args constructor + } + + } + + static class BasicLocaleProvider implements LocaleProvider { + + @Override + public Locale get() { + return Locale.FRENCH; + } + + } + + static class ReturnsNullLocaleProvider implements LocaleProvider { + + @Override + public Locale get() { + return null; + } + + } + + static class BadConstructorLocaleProvider implements LocaleProvider { + + private final String language; + + BadConstructorLocaleProvider(String language) { + this.language = language; + } + + @Override + public Locale get() { + return Locale.forLanguageTag(language); + } + + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java new file mode 100644 index 000000000000..ac1beddb600a --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java @@ -0,0 +1,378 @@ +/* + * 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.engine.extension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestClass; +import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestMethod; +import static org.junit.jupiter.testkit.assertion.JUnitJupiterAssert.assertThat; + +import java.util.TimeZone; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtensionConfigurationException; +import org.junit.jupiter.api.util.DefaultTimeZone; +import org.junit.jupiter.api.util.ReadsDefaultTimeZone; +import org.junit.jupiter.api.util.TimeZoneProvider; +import org.junit.jupiter.testkit.ExecutionResults; + +@DisplayName("DefaultTimeZone extension") +class DefaultTimeZoneTests { + + private static TimeZone TEST_DEFAULT_TIMEZONE; + private static TimeZone DEFAULT_TIMEZONE_BEFORE_TEST; + + @BeforeAll + static void globalSetUp() { + // we set UTC as test time zone unless it is already + // the system's time zone; in that case we use UTC+12 + DEFAULT_TIMEZONE_BEFORE_TEST = TimeZone.getDefault(); + TimeZone utc = TimeZone.getTimeZone("UTC"); + TimeZone utcPlusTwelve = TimeZone.getTimeZone("GMT+12:00"); + if (DEFAULT_TIMEZONE_BEFORE_TEST.equals(utc)) + TimeZone.setDefault(utcPlusTwelve); + else + TimeZone.setDefault(utc); + TEST_DEFAULT_TIMEZONE = TimeZone.getDefault(); + } + + @AfterAll + static void globalTearDown() { + TimeZone.setDefault(DEFAULT_TIMEZONE_BEFORE_TEST); + } + + @Nested + @DisplayName("when applied on the method level") + class MethodLevelTests { + + @Test + @ReadsDefaultTimeZone + @DisplayName("does nothing when annotation is not present") + void doesNothingWhenAnnotationNotPresent() { + assertThat(TimeZone.getDefault()).isEqualTo(TEST_DEFAULT_TIMEZONE); + } + + @Test + @DefaultTimeZone("GMT") + @DisplayName("does not throw when explicitly set to GMT") + void doesNotThrowForExplicitGmt() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT")); + } + + @Test + @DefaultTimeZone("CET") + @DisplayName("sets the default time zone using an abbreviation") + void setsTimeZoneFromAbbreviation() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET")); + } + + @Test + @DefaultTimeZone("America/Los_Angeles") + @DisplayName("sets the default time zone using a full name") + void setsTimeZoneFromFullName() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("America/Los_Angeles")); + } + + } + + @Nested + @DefaultTimeZone("GMT-8:00") + @DisplayName("when applied on the class level") + class ClassLevelTestCases { + + @Test + @ReadsDefaultTimeZone + @DisplayName("sets the default time zone") + void shouldExecuteWithClassLevelTimeZone() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT-8:00")); + } + + @Test + @DefaultTimeZone("GMT-12:00") + @DisplayName("gets overridden by annotation on the method level") + void shouldBeOverriddenWithMethodLevelTimeZone() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT-12:00")); + } + + } + + @Nested + @DefaultTimeZone("GMT") + @DisplayName("when explicitly set to GMT on the class level") + class ExplicitGmtClassLevelTestCases { + + @Test + @DisplayName("does not throw and sets to GMT ") + void explicitGmt() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT")); + } + + } + + @Nested + @DefaultTimeZone("GMT-8:00") + @DisplayName("with nested classes") + class NestedTests { + + @Nested + @DisplayName("without DefaultTimeZone annotation") + class NestedClass { + + @Test + @ReadsDefaultTimeZone + @DisplayName("DefaultTimeZone should be set from enclosed class when it is not provided in nested") + public void shouldSetTimeZoneFromEnclosedClass() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT-8:00")); + } + + } + + @Nested + @DefaultTimeZone("GMT-12:00") + @DisplayName("with DefaultTimeZone annotation") + class AnnotatedNestedClass { + + @Test + @ReadsDefaultTimeZone + @DisplayName("DefaultTimeZone should be set from nested class when it is provided") + public void shouldSetTimeZoneFromNestedClass() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT-12:00")); + } + + @Test + @DefaultTimeZone("GMT-6:00") + @DisplayName("DefaultTimeZone should be set from method when it is provided") + public void shouldSetTimeZoneFromMethodOfNestedClass() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT-6:00")); + } + + } + + } + + @Nested + @DefaultTimeZone("GMT-12:00") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class ResettingDefaultTimeZoneTests { + + @Nested + @DefaultTimeZone("GMT-3:00") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class ResettingDefaultTimeZoneNestedTests { + + @Test + @DefaultTimeZone("GMT+6:00") + void setForTestMethod() { + // only here to set the time zone, so another test can verify whether it was reset; + // still, better to assert the value was actually set + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT+6:00")); + } + + @AfterAll + @ReadsDefaultTimeZone + void resetAfterTestMethodExecution() { + assertThat(TimeZone.getDefault()).isEqualTo(TEST_DEFAULT_TIMEZONE); + } + + } + + @AfterAll + @ReadsDefaultTimeZone + void resetAfterTestMethodExecution() { + assertThat(TimeZone.getDefault()).isEqualTo(TEST_DEFAULT_TIMEZONE); + } + + } + + @Nested + @DisplayName("when misconfigured") + class ConfigurationTests { + + @Test + @ReadsDefaultTimeZone + @DisplayName("on method level, throws exception") + void throwsWhenConfigurationIsBad() { + ExecutionResults results = executeTestMethod(BadMethodLevelConfigurationTestCases.class, + "badConfiguration"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageNotContaining( + "should never execute").hasMessageContaining("@DefaultTimeZone not configured correctly."); + } + + @Test + @ReadsDefaultTimeZone + @DisplayName("on class level, throws exception") + void shouldThrowWithBadConfiguration() { + ExecutionResults results = executeTestClass(BadClassLevelConfigurationTestCases.class); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "@DefaultTimeZone not configured correctly."); + } + + @AfterEach + void verifyMisconfigurationSisNotChangeTimeZone() { + assertThat(TimeZone.getDefault()).isEqualTo(TEST_DEFAULT_TIMEZONE); + } + + } + + static class BadMethodLevelConfigurationTestCases { + + @Test + @DefaultTimeZone("Gibberish") + void badConfiguration() { + } + + } + + @DefaultTimeZone("Gibberish") + static class BadClassLevelConfigurationTestCases { + + @Test + void badConfiguration() { + } + + } + + @Nested + @DisplayName("used with inheritance") + class InheritanceTests extends InheritanceBaseTest { + + @Test + @DisplayName("should inherit default time zone annotation") + void shouldInheritClearAndSetProperty() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT-8:00")); + } + + } + + @DefaultTimeZone("GMT-8:00") + static class InheritanceBaseTest { + + } + + @Nested + @DisplayName("used with TimeZoneProvider") + class ProviderTests { + + @Test + @DisplayName("can get a basic time zone") + @DefaultTimeZone(timeZoneProvider = BasicTimeZoneProvider.class) + void canGetBasicTimeZone() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Europe/Prague")); + } + + @Test + @DisplayName("defaults to GMT if the provider returns null") + @DefaultTimeZone(timeZoneProvider = NullProvider.class) + void defaultToGmt() { + assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("GMT")); + } + + @Test + @ReadsDefaultTimeZone + @DisplayName("throws ExtensionConfigurationException if the provider is not the only option") + void throwsForMutuallyExclusiveOptions() { + ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "notExclusive"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "Either a valid time zone id or a TimeZoneProvider must be provided"); + } + + @Test + @ReadsDefaultTimeZone + @DisplayName("throws ExtensionConfigurationException if properties are empty") + void throwsForEmptyOptions() { + ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "empty"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "Either a valid time zone id or a TimeZoneProvider must be provided"); + } + + @Test + @ReadsDefaultTimeZone + @DisplayName("throws ExtensionConfigurationException if the provider does not have a suitable constructor") + void throwsForBadConstructor() { + ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "noConstructor"); + + assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( + ExtensionConfigurationException.class).hasMessageContaining( + "Could not instantiate TimeZoneProvider because of exception"); + } + + } + + static class BadTimeZoneProviderTestCases { + + @Test + @DefaultTimeZone(value = "GMT", timeZoneProvider = BasicTimeZoneProvider.class) + void notExclusive() { + // can't have both a time zone value and a provider + } + + @Test + @DefaultTimeZone + void empty() { + // must have a provider or a time zone + } + + @Test + @DefaultTimeZone(timeZoneProvider = ComplicatedProvider.class) + void noConstructor() { + // provider has to have a no-args constructor + } + + } + + static class BasicTimeZoneProvider implements TimeZoneProvider { + + @Override + public TimeZone get() { + return TimeZone.getTimeZone("Europe/Prague"); + } + + } + + static class NullProvider implements TimeZoneProvider { + + @Override + public TimeZone get() { + return null; + } + + } + + static class ComplicatedProvider implements TimeZoneProvider { + + private final String timeZoneString; + + public ComplicatedProvider(String timeZoneString) { + this.timeZoneString = timeZoneString; + } + + @Override + public TimeZone get() { + return TimeZone.getTimeZone(timeZoneString); + } + + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/ExecutionResults.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/ExecutionResults.java new file mode 100644 index 000000000000..6207e3a2a842 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/ExecutionResults.java @@ -0,0 +1,152 @@ +/* + * 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.testkit; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.Events; + +/** + * Pioneers' class to handle JUnit Jupiter's {@link org.junit.platform.testkit.engine.EngineExecutionResults}. + * + *

Instantiate with the static factory methods in {@link JUnitJupiterTestKit}. + */ +public class ExecutionResults { + + private final EngineExecutionResults executionResults; + + private static final String JUPITER_ENGINE_NAME = "junit-jupiter"; + + static class Builder { + + private final Map additionalProperties = new HashMap<>(); + private final List selectors = new ArrayList<>(); + + Builder addConfigurationParameters(Map additionalConfig) { + additionalProperties.putAll(additionalConfig); + return this; + } + + Executor selectTestClass(Class testClass) { + selectors.add(DiscoverySelectors.selectClass(testClass)); + return new Executor(); + } + + Executor selectTestClasses(Iterable> testClasses) { + StreamSupport.stream(testClasses.spliterator(), false).map(DiscoverySelectors::selectClass).forEach( + selectors::add); + return new Executor(); + } + + Executor selectTestMethod(Class testClass, String testMethodName) { + selectors.add(DiscoverySelectors.selectMethod(testClass, testMethodName)); + return new Executor(); + } + + Executor selectTestMethodWithParameterTypes(Class testClass, String testMethodName, + String methodParameterTypes) { + selectors.add(DiscoverySelectors.selectMethod(testClass, testMethodName, methodParameterTypes)); + return new Executor(); + } + + Executor selectNestedTestClass(List> enclosingClasses, Class testClass) { + selectors.add(DiscoverySelectors.selectNestedClass(enclosingClasses, testClass)); + return new Executor(); + } + + Executor selectNestedTestMethod(List> enclosingClasses, Class testClass, String testMethodName) { + selectors.add(DiscoverySelectors.selectNestedMethod(enclosingClasses, testClass, testMethodName)); + return new Executor(); + } + + Executor selectNestedTestMethodWithParameterTypes(List> enclosingClasses, Class testClass, + String testMethodName, String methodParameterTypes) { + selectors.add(DiscoverySelectors.selectNestedMethod(enclosingClasses, testClass, testMethodName, + methodParameterTypes)); + return new Executor(); + } + + class Executor { + + ExecutionResults execute() { + return new ExecutionResults(Builder.this.additionalProperties, Builder.this.selectors); + } + + } + + } + + private ExecutionResults(Map additionalProperties, List selectors) { + this.executionResults = getConfiguredJupiterEngine().configurationParameters(additionalProperties).selectors( + selectors.toArray(DiscoverySelector[]::new)).execute(); + } + + static Builder builder() { + return new Builder(); + } + + private EngineTestKit.Builder getConfiguredJupiterEngine() { + return EngineTestKit.engine(JUPITER_ENGINE_NAME) + // to tease out concurrency-related bugs, we want parallel execution of our tests + // (for details, see section "Thread-safety" in CONTRIBUTING.adoc) + .configurationParameter("junit.jupiter.execution.parallel.enabled", "true").configurationParameter( + "junit.jupiter.execution.parallel.mode.default", "concurrent") + // since we have full control over which tests we execute with this engine, + // we can parallelize more aggressively than in the general settings in `junit-platform.properties` + .configurationParameter("junit.jupiter.execution.parallel.mode.classes.default", + "concurrent").configurationParameter("junit.jupiter.execution.parallel.config.strategy", + "dynamic").configurationParameter("junit.jupiter.execution.parallel.config.dynamic.factor", + "1"); + } + + /** + * Get all recorded events. + */ + public Events allEvents() { + return executionResults.allEvents(); + } + + /** + * Get recorded dynamically registered events. + */ + public Events dynamicallyRegisteredEvents() { + return executionResults.allEvents().dynamicallyRegistered(); + } + + /** + * Get recorded events for containers. + * + *

In this context, the word "container" applies to {@link org.junit.platform.engine.TestDescriptor + * TestDescriptors} that return {@code true} from {@link org.junit.platform.engine.TestDescriptor#isContainer()}.

+ */ + public Events containerEvents() { + return executionResults.containerEvents(); + } + + /** + * Get recorded events for tests. + * + *

In this context, the word "test" applies to {@link org.junit.platform.engine.TestDescriptor + * TestDescriptors} that return {@code true} from {@link org.junit.platform.engine.TestDescriptor#isTest()}.

+ */ + public Events testEvents() { + return executionResults.testEvents(); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java new file mode 100644 index 000000000000..9314160790a7 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java @@ -0,0 +1,120 @@ +/* + * 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.testkit; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.testkit.assertion.JUnitJupiterAssert.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("Execute") +class JUnitJupiterKitTests { + + @Test + @DisplayName("all tests of a class") + void executeTestClass() { + ExecutionResults results = JUnitJupiterTestKit.executeTestClass(DummyClass.class); + + assertThat(results).hasNumberOfStartedTests(1); + } + + @Test + @DisplayName("all tests of all given classes") + void executeTestClasses() { + ExecutionResults results = JUnitJupiterTestKit.executeTestClasses( + asList(DummyClass.class, SecondDummyClass.class)); + + assertThat(results).hasNumberOfStartedTests(2); + } + + @Test + @DisplayName("a specific method") + void executeTestMethod() { + ExecutionResults results = JUnitJupiterTestKit.executeTestMethod(DummyClass.class, "nothing"); + + assertThat(results).hasNumberOfStartedTests(1); + } + + @Nested + @DisplayName("a specific parametrized method") + class ExecuteTestMethodWithParametersTests { + + @Test + @DisplayName(" where parameter is a single class") + void executeTestMethodWithParameterTypes_singleParameterType() { + ExecutionResults results = JUnitJupiterTestKit.executeTestMethodWithParameterTypes(DummyPropertyClass.class, + "single", String.class); + + assertThat(results).hasNumberOfStartedTests(1); + } + + @Test + @DisplayName(" where parameter is an array of classes") + void executeTestMethodWithParameterTypes_parameterTypeAsArray() { + Class[] classes = { String.class }; + + ExecutionResults results = JUnitJupiterTestKit.executeTestMethodWithParameterTypes(DummyPropertyClass.class, + "single", classes); + + assertThat(results).hasNumberOfStartedTests(1); + } + + @Test + @DisplayName("without parameter results in IllegalArgumentException") + void executeTestMethodWithParameterTypes_parameterArrayIsNull_NullPointerException() { + assertThatThrownBy(() -> JUnitJupiterTestKit.executeTestMethodWithParameterTypes(DummyPropertyClass.class, + "single", (Class) null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("without parameter results in IllegalArgumentException") + void executeTestMethodWithParameterTypes_singleParameterIsNull_IllegalArgumentException() { + assertThatThrownBy(() -> JUnitJupiterTestKit.executeTestMethodWithParameterTypes(DummyPropertyClass.class, + "single", (Class[]) null)).isInstanceOf(IllegalArgumentException.class).hasMessage( + "methodParameterTypes must not be null"); + } + + } + + static class DummyPropertyClass { + + @ParameterizedTest(name = "See if enabled with {0}") + @ValueSource(strings = { "parameter" }) + void single(String reason) { + // Do nothing + } + + } + + static class DummyClass { + + @Test + void nothing() { + // Do nothing + } + + } + + static class SecondDummyClass { + + @Test + void nothing() { + // Do nothing + } + + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterTestKit.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterTestKit.java new file mode 100644 index 000000000000..6bfd575ac3fc --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterTestKit.java @@ -0,0 +1,163 @@ +/* + * 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.testkit; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import java.util.List; +import java.util.Map; + +import org.opentest4j.TestAbortedException; + +public class JUnitJupiterTestKit { + + /** + * Returns the execution results of the given test class. + * + * @param testClass The test class instance + * @return The execution results + */ + public static ExecutionResults executeTestClass(Class testClass) { + return ExecutionResults.builder().selectTestClass(testClass).execute(); + } + + /** + * Returns the execution results of the given test classes. + * + * @param testClasses The collection of test class instances + * @return The execution results + */ + public static ExecutionResults executeTestClasses(Iterable> testClasses) { + return ExecutionResults.builder().selectTestClasses(testClasses).execute(); + } + + /** + * Returns the execution results of the given method of a given test class. + * + * @param testClass The test class instance + * @param testMethodName Name of the test method (of the given class) + * @return The execution results + */ + public static ExecutionResults executeTestMethod(Class testClass, String testMethodName) { + return ExecutionResults.builder().selectTestMethod(testClass, testMethodName).execute(); + } + + /** + * Returns the execution results of the given method of a given test class. + * + * @param testClass The test class instance + * @param testMethodName Name of the test method (of the given class) + * @param methodParameterTypes Class type(s) of the parameter(s) + * @return The execution results + * @throws IllegalArgumentException when methodParameterTypes is null + * This method only checks parameters which are not part of the underlying + * Jupiter TestKit. The Jupiter TestKit may throw other exceptions! + */ + public static ExecutionResults executeTestMethodWithParameterTypes(Class testClass, String testMethodName, + Class... methodParameterTypes) { + + String allTypeNames = toMethodParameterTypesString(methodParameterTypes); + + return ExecutionResults.builder().selectTestMethodWithParameterTypes(testClass, testMethodName, + allTypeNames).execute(); + } + + /** + * Returns the execution results of the given nested test class. + * + * @param enclosingClasses List of the enclosing classes + * @param testClass Name of the test class, the results should be returned + * @return The execution results + */ + public static ExecutionResults executeNestedTestClass(List> enclosingClasses, Class testClass) { + return ExecutionResults.builder().selectNestedTestClass(enclosingClasses, testClass).execute(); + } + + /** + * Returns the execution results of the given method of a given nested test class. + * + * @param enclosingClasses List of the enclosing classes + * @param testClass Name of the test class + * @param testMethodName Name of the test method (of the given class) + * @return The execution results + */ + public static ExecutionResults executeNestedTestMethod(List> enclosingClasses, Class testClass, + String testMethodName) { + return ExecutionResults.builder().selectNestedTestMethod(enclosingClasses, testClass, testMethodName).execute(); + } + + /** + * Returns the execution results of the given method of a given nested test class. + * + * @param enclosingClasses List of the enclosing classes + * @param testClass Name of the test class + * @param testMethodName Name of the test method (of the given class) + * @param methodParameterTypes Class type(s) of the parameter(s) + * @return The execution results + * @throws IllegalArgumentException when methodParameterTypes is null + * This method only checks parameters which are not part of the underlying + * Jupiter TestKit. The Jupiter TestKit may throw other exceptions! + */ + public static ExecutionResults executeNestedTestMethodWithParameterTypes(List> enclosingClasses, + Class testClass, String testMethodName, Class... methodParameterTypes) { + + String allTypeNames = toMethodParameterTypesString(methodParameterTypes); + + return ExecutionResults.builder().selectNestedTestMethodWithParameterTypes(enclosingClasses, testClass, + testMethodName, allTypeNames).execute(); + } + + /** + * Returns the execution results of the given method of a given test class + * and passes the additional configuration parameters. + * + * @param configurationParameters additional configuration parameters + * @param testClass The test class instance + * @param testMethodName Name of the test method (of the given class) + * @param methodParameterTypes Class type(s) of the parameter(s) + * @return The execution results + * @throws IllegalArgumentException when methodParameterTypes is null + * This method only checks parameters which are not part of the underlying + * Jupiter TestKit. The Jupiter TestKit may throw other exceptions! + */ + public static ExecutionResults executeTestMethodWithParameterTypesAndConfigurationParameters( + Map configurationParameters, Class testClass, String testMethodName, + Class... methodParameterTypes) { + + String allTypeNames = toMethodParameterTypesString(methodParameterTypes); + + return ExecutionResults.builder().addConfigurationParameters( + configurationParameters).selectTestMethodWithParameterTypes(testClass, testMethodName, + allTypeNames).execute(); + } + + private static String toMethodParameterTypesString(Class... methodParameterTypes) { + // throw IllegalArgumentException for a `null` array instead of NPE + // (hence no use of `Objects::requireNonNull`) + if (methodParameterTypes == null) { + throw new IllegalArgumentException("methodParameterTypes must not be null"); + } + + // Concatenating all type names, because DiscoverySelectors.selectMethod only takes String as a parameter. + return stream(methodParameterTypes).map(Class::getName).collect(joining(",")); + } + + /** + * Aborts the test execution. Makes the test code a little nicer. + * + * @throws TestAbortedException always throws this + */ + public static void abort() { + throw new TestAbortedException(); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java new file mode 100644 index 000000000000..36f1fe49223d --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java @@ -0,0 +1,38 @@ +/* + * 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.testkit.assertion; + +import org.assertj.core.api.AbstractAssert; + +/** + * A very basic extension of the AbstractAssert, used to add a quantity to assertions. + * By storing this value in a field we don't have to refer back to it every time. + * + *

Instead of assertThat(results).hasTests().thatStarted(3).thenFailed(3) we can write + * assertThat(results).hasNumberOfTests(3).thatStarted().andAllOfThemFailed().

+ * + * @param the "self" type of this assertion class. Please read + * " + * Emulating 'self types' using Java Generics to simplify fluent API implementation" + * for more details. + * @param the type of the "actual" value. + */ +abstract class AbstractJUnitJupiterAssert, ACTUAL> + extends AbstractAssert { + + protected final int expected; + + protected AbstractJUnitJupiterAssert(ACTUAL actual, Class self, int expected) { + super(actual, self); + this.expected = expected; + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java new file mode 100644 index 000000000000..301fda1707a8 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java @@ -0,0 +1,21 @@ +/* + * 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.testkit.assertion; + +import org.junit.jupiter.testkit.assertion.reportentry.ReportEntryAssert; +import org.junit.jupiter.testkit.assertion.single.TestCaseAssert; +import org.junit.jupiter.testkit.assertion.suite.TestSuiteAssert; + +/** + * Base interface for all {@link org.junit.jupiter.testkit.ExecutionResults} assertions. + */ +public interface ExecutionResultAssert extends TestSuiteAssert, TestCaseAssert, ReportEntryAssert { +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java new file mode 100644 index 000000000000..1f71dc0010a0 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java @@ -0,0 +1,45 @@ +/* + * 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.testkit.assertion; + +import java.nio.file.Path; +import java.util.Properties; + +import org.junit.jupiter.testkit.ExecutionResults; + +/** + * Entry point to all JUnit Pioneer assertions. + */ +public class JUnitJupiterAssert { + + private JUnitJupiterAssert() { + // private constructor to prevent instantiation + } + + public static ExecutionResultAssert assertThat(ExecutionResults actual) { + return new JUnitJupiterExecutionResultAssert(actual); + } + + public static JUnitJupiterPathAssert assertThat(Path actual) { + return new JUnitJupiterPathAssert(actual); + } + + /** + * Make an assertion on a {@link Properties} instance. + * + * @param actual The {@link Properties} instance the assertion is made with respect to + * @return Assertion instance + */ + public static PropertiesAssert assertThat(Properties actual) { + return new PropertiesAssert(actual); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java new file mode 100644 index 000000000000..ebe974c07497 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java @@ -0,0 +1,317 @@ +/* + * 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.testkit.assertion; + +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ListAssert; +import org.junit.jupiter.testkit.ExecutionResults; +import org.junit.jupiter.testkit.assertion.reportentry.ReportEntryContentAssert; +import org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert; +import org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert; +import org.junit.jupiter.testkit.assertion.suite.TestSuiteAssert; +import org.junit.jupiter.testkit.assertion.suite.TestSuiteContainersAssert; +import org.junit.jupiter.testkit.assertion.suite.TestSuiteTestsAssert; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.testkit.engine.Events; + +class JUnitJupiterExecutionResultAssert extends AbstractAssert + implements ExecutionResultAssert, TestSuiteAssert, TestSuiteTestsAssert.TestSuiteTestsFailureAssert, + TestSuiteContainersAssert.TestSuiteContainersFailureAssert { + + private boolean test = true; + + JUnitJupiterExecutionResultAssert(ExecutionResults actual) { + super(actual, JUnitJupiterExecutionResultAssert.class); + } + + @Override + public ReportEntryContentAssert hasNumberOfReportEntries(int expected) { + try { + List> entries = reportEntries(); + Assertions.assertThat(entries).hasSize(expected); + Integer[] ones = IntStream.generate(() -> 1).limit(expected).boxed().toArray(Integer[]::new); + Assertions.assertThat(entries).extracting(Map::size).containsExactly(ones); + + List> entryList = entries.stream().flatMap( + map -> map.entrySet().stream()).collect(toList()); + + return new ReportEntryAssertBase(entryList, expected); + } + catch (AssertionError error) { + getAllExceptions(actual.allEvents().reportingEntryPublished()).forEach(error::addSuppressed); + throw error; + } + } + + @Override + public ReportEntryContentAssert hasSingleReportEntry() { + return hasNumberOfReportEntries(1); + } + + @Override + public void hasNoReportEntries() { + try { + Assertions.assertThat(reportEntries()).isEmpty(); + } + catch (AssertionError error) { + getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); + throw error; + } + } + + @Override + public TestCaseStartedAssert hasSingleStartedTest() { + Events events = actual.testEvents(); + assertSingleTest(events.started()); + // Don't filter started() here; this would prevent further assertions on test outcome on + // returned assert because outcome is reported for the FINISHED event, not the STARTED one + return new TestCaseAssertBase(events); + } + + @Override + public TestCaseFailureAssert hasSingleFailedTest() { + return assertSingleTest(actual.testEvents().failed()); + } + + @Override + public void hasSingleAbortedTest() { + assertSingleTest(actual.testEvents().aborted()); + } + + @Override + public void hasSingleSucceededTest() { + assertSingleTest(actual.testEvents().succeeded()); + } + + @Override + public void hasSingleSkippedTest() { + assertSingleTest(actual.testEvents().skipped()); + } + + @Override + public TestCaseStartedAssert hasSingleDynamicallyRegisteredTest() { + Events events = actual.testEvents(); + assertSingleTest(events.dynamicallyRegistered()); + // Don't filter dynamicallyRegistered() here; this would prevent further assertions on test outcome on + // returned assert because outcome is reported for the FINISHED event, not the DYNAMIC_TEST_REGISTERED one + return new TestCaseAssertBase(events); + } + + private TestCaseAssertBase assertSingleTest(Events events) { + try { + Assertions.assertThat(events.count()).isEqualTo(1); + } + catch (AssertionError error) { + getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); + throw error; + } + return new TestCaseAssertBase(events); + } + + @Override + public TestCaseStartedAssert hasSingleStartedContainer() { + Events events = actual.containerEvents(); + assertSingleTest(events.started()); + // Don't filter started() here; this would prevent further assertions on container outcome on + // returned assert because outcome is reported for the FINISHED event, not the STARTED one + return new TestCaseAssertBase(events); + } + + @Override + public TestCaseFailureAssert hasSingleFailedContainer() { + return assertSingleContainer(actual.containerEvents().failed()); + } + + @Override + public void hasSingleAbortedContainer() { + assertSingleContainer(actual.containerEvents().aborted()); + } + + @Override + public void hasSingleSucceededContainer() { + assertSingleContainer(actual.containerEvents().succeeded()); + } + + @Override + public void hasSingleSkippedContainer() { + assertSingleContainer(actual.containerEvents().skipped()); + } + + @Override + public TestCaseStartedAssert hasSingleDynamicallyRegisteredContainer() { + Events events = actual.containerEvents(); + assertSingleContainer(events.dynamicallyRegistered()); + // Don't filter dynamicallyRegistered() here; this would prevent further assertions on container outcome on + // returned assert because outcome is reported for the FINISHED event, not the DYNAMIC_TEST_REGISTERED one + return new TestCaseAssertBase(events); + } + + private TestCaseAssertBase assertSingleContainer(Events events) { + try { + Assertions.assertThat(events.count()).isEqualTo(1); + } + catch (AssertionError error) { + getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); + throw error; + } + return new TestCaseAssertBase(events); + } + + @Override + public TestSuiteTestsAssert hasNumberOfStartedTests(int expected) { + return hasNumberOfSpecificTests(actual.testEvents().started().count(), expected); + } + + @Override + public TestSuiteTestsFailureAssert hasNumberOfFailedTests(int expected) { + this.test = true; + return hasNumberOfSpecificTests(actual.testEvents().failed().count(), expected); + } + + @Override + public TestSuiteTestsAssert hasNumberOfAbortedTests(int expected) { + return hasNumberOfSpecificTests(actual.testEvents().aborted().count(), expected); + } + + @Override + public TestSuiteTestsAssert hasNumberOfSucceededTests(int expected) { + return hasNumberOfSpecificTests(actual.testEvents().succeeded().count(), expected); + } + + @Override + public TestSuiteTestsAssert hasNumberOfSkippedTests(int expected) { + return hasNumberOfSpecificTests(actual.testEvents().skipped().count(), expected); + } + + @Override + public TestSuiteTestsAssert hasNumberOfDynamicallyRegisteredTests(int expected) { + return hasNumberOfSpecificTests(actual.testEvents().dynamicallyRegistered().count(), expected); + } + + private TestSuiteTestsFailureAssert hasNumberOfSpecificTests(long tests, int expected) { + try { + Assertions.assertThat(tests).isEqualTo(expected); + return this; + } + catch (AssertionError error) { + getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); + throw error; + } + } + + @Override + public TestSuiteContainersAssert hasNumberOfStartedContainers(int expected) { + return hasNumberOfSpecificContainers(actual.containerEvents().started().count(), expected); + } + + @Override + public TestSuiteContainersFailureAssert hasNumberOfFailedContainers(int expected) { + this.test = false; + return hasNumberOfSpecificContainers(actual.containerEvents().failed().count(), expected); + } + + @Override + public TestSuiteContainersAssert hasNumberOfAbortedContainers(int expected) { + return hasNumberOfSpecificContainers(actual.containerEvents().aborted().count(), expected); + } + + @Override + public TestSuiteContainersAssert hasNumberOfSucceededContainers(int expected) { + return hasNumberOfSpecificContainers(actual.containerEvents().succeeded().count(), expected); + } + + @Override + public TestSuiteContainersAssert hasNumberOfSkippedContainers(int expected) { + return hasNumberOfSpecificContainers(actual.containerEvents().skipped().count(), expected); + } + + @Override + public TestSuiteContainersAssert hasNumberOfDynamicallyRegisteredContainers(int expected) { + return hasNumberOfSpecificContainers(actual.containerEvents().dynamicallyRegistered().count(), expected); + } + + private TestSuiteContainersFailureAssert hasNumberOfSpecificContainers(long containers, int expected) { + try { + Assertions.assertThat(containers).isEqualTo(expected); + return this; + } + catch (AssertionError error) { + getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); + throw error; + } + } + + private List> reportEntries() { + return actual.allEvents().reportingEntryPublished().stream().map( + event -> event.getPayload(ReportEntry.class)).filter(Optional::isPresent).map(Optional::get).map( + ReportEntry::getKeyValuePairs).collect(toList()); + } + + @Override + public final ListAssert withExceptionInstancesOf(Class exceptionType) { + return assertExceptions(events -> { + Stream> classStream = getAllExceptions(events).map(Throwable::getClass); + Assertions.assertThat(classStream).containsOnly(exceptionType); + }); + } + + @Override + public ListAssert withExceptions() { + return assertExceptions( + events -> Assertions.assertThat(events.failed().count()).isEqualTo(getAllExceptions(events).count())); + } + + private ListAssert assertExceptions(Consumer assertion) { + try { + Events events = getProperEvents(); + assertion.accept(events); + return new ListAssert<>(getAllExceptions(events).map(Throwable::getMessage)); + } + catch (AssertionError error) { + getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); + throw error; + } + } + + private Events getProperEvents() { + return this.test ? actual.testEvents() : actual.containerEvents(); + } + + static Stream getAllExceptions(Events events) { + return events.stream().map(fail -> fail.getPayload(TestExecutionResult.class)).filter(Optional::isPresent).map( + Optional::get).map(TestExecutionResult::getThrowable).filter(Optional::isPresent).map(Optional::get); + } + + @Override + public void assertingExceptions(Predicate> predicate) { + List thrownExceptions = getAllExceptions(getProperEvents()).collect(toList()); + Assertions.assertThat(predicate).accepts(thrownExceptions); + } + + @Override + public void andThenCheckExceptions(Consumer> testFunction) { + List thrownExceptions = getAllExceptions(getProperEvents()).collect(toList()); + testFunction.accept(thrownExceptions); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java new file mode 100644 index 000000000000..c53fa73e7add --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java @@ -0,0 +1,64 @@ +/* + * 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.testkit.assertion; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +import org.assertj.core.api.PathAssert; + +public class JUnitJupiterPathAssert extends PathAssert { + + JUnitJupiterPathAssert(Path path) { + super(path); + } + + public JUnitJupiterPathAssert canReadAndWriteFile() { + isNotNull(); + + Path textFile; + try { + textFile = Files.createTempFile(actual, "some-text-file", ".txt"); + } + catch (IOException e1) { + throw failure("Cannot create a file"); + } + + String expectedText = "some-text"; + try { + Files.write(textFile, List.of(expectedText)); + } + catch (IOException e) { + throw failure("Cannot write to a file"); + } + + String actualText; + try { + actualText = new String(Files.readAllBytes(textFile), UTF_8).trim(); + } + catch (IOException e) { + throw failure("Cannot read from a file"); + } + + if (!Objects.equals(actualText, expectedText)) { + throw failureWithActualExpected(actualText, expectedText, "File expected to contain <%s>, but was <%s>", + expectedText, actualText); + } + + return this; + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java new file mode 100644 index 000000000000..76685dbfc4e8 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java @@ -0,0 +1,194 @@ +/* + * 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.testkit.assertion; + +import java.lang.reflect.Field; +import java.util.Properties; + +import org.assertj.core.api.AbstractAssert; +import org.junit.platform.commons.support.HierarchyTraversalMode; +import org.junit.platform.commons.support.ReflectionSupport; + +/** + * Allows comparison of {@link Properties} with optional awareness of their structure, + * rather than just treating them as Maps. Object values, which are marginally supported + * by {@code Properties}, are supported in assertions as much as possible. + */ +public class PropertiesAssert extends AbstractAssert { + + PropertiesAssert(Properties actual) { + super(actual, PropertiesAssert.class); + } + + /** + * Assert Properties has the same effective values as the passed instance, but not + * the same nested default structure. + * + *

Properties are considered effectively equal if they have the same property + * names returned by {@code Properties.propertyNames()} and the same values returned by + * {@code getProperty(name)}. Properties may come from the properties instance itself, + * or from a nested default instance, indiscriminately. + * + *

Properties partially supports object values, but return null for {@code getProperty(name)} + * when the value is a non-string. This assertion follows the same rules: Any non-String + * value is considered null for comparison purposes. + * + * @param expected The actual is expected to be effectively the same as this Properties + * @return Assertion instance + */ + public PropertiesAssert isEffectivelyEqualsTo(Properties expected) { + + // Compare values present in actual + actual.propertyNames().asIterator().forEachRemaining(k -> { + + String kStr = k.toString(); + + String actValue = actual.getProperty(kStr); + String expValue = expected.getProperty(kStr); + + if (actValue == null) { + if (expValue != null) { + // An object value is the only way to get a null from getProperty() + throw failure("For the property '<%s>', " + + "the actual value was an object but the expected the string '<%s>'.", + k, expValue); + } + } + else if (!actValue.equals(expValue)) { + throw failure("For the property '<%s>', the actual value was <%s> but <%s> was expected", k, actValue, + expValue); + } + }); + + // Compare values present in expected - Anything not matching must not have been present in actual + expected.propertyNames().asIterator().forEachRemaining(k -> { + + String kStr = k.toString(); + + String actValue = actual.getProperty(kStr); + String expValue = expected.getProperty(kStr); + + if (expValue == null) { + if (actValue != null) { + + // An object value is the only way to get a null from getProperty() + throw failure("For the property '<%s>', " + + "the actual value was the string '<%s>', but an object was expected.", + k, actValue); + } + } + else if (!expValue.equals(actValue)) { + throw failure("The property <%s> was expected to be <%s>, but was missing", k, expValue); + } + }); + + return this; + } + + /** + * The converse of isEffectivelyEqualTo. + * + * @param expected The actual is expected to NOT be effectively equal to this Properties + * @return Assertion instance + */ + public PropertiesAssert isNotEffectivelyEqualTo(Properties expected) { + try { + isEffectivelyEqualsTo(expected); + } + catch (AssertionError ae) { + return this; // Expected + } + + throw failure("The actual Properties should not be effectively equal to the expected one."); + } + + /** + * Compare values directly present in Properties and recursively into default Properties. + * + * @param expected The actual is expected to be strictly equal to this Properties + * @return Assertion instance + */ + public PropertiesAssert isStrictlyEqualTo(Properties expected) { + + // Compare values present in actual + actual.keySet().forEach(k -> { + if (!actual.get(k).equals(expected.get(k))) { + throw failure("For the property <%s> the actual value was <%s> but <%s> was expected", k, actual.get(k), + expected.get(k)); + } + }); + + // Compare values present in expected - Anything not matching must not have been present in actual + expected.keySet().forEach(k -> { + if (!expected.get(k).equals(actual.get(k))) { + throw failure("The property <%s> was expected to be <%s>, but was missing", k, expected.get(k)); + } + }); + + // Dig down into the nested defaults + Properties actualDefault = getDefaultFieldValue(actual); + Properties expectedDefault = getDefaultFieldValue(expected); + + if (actualDefault != null && expectedDefault != null) { + return new PropertiesAssert(actualDefault).isStrictlyEqualTo(expectedDefault); + } + else if (actualDefault != null) { + throw failure("The actual Properties had non-null defaults, but none were expected"); + } + else if (expectedDefault != null) { + throw failure("The expected Properties had non-null defaults, but none were in actual"); + } + + return this; + } + + /** + * Simple converse of isStrictlyEqualTo. + * + * @param expected The actual is expected to NOT be strictly equal to this Properties + * @return Assertion instance + */ + public PropertiesAssert isNotStrictlyEqualTo(Properties expected) { + try { + isStrictlyEqualTo(expected); + } + catch (AssertionError ae) { + return this; // Expected + } + + throw failure("The actual Properties should not be strictly the same as the expected one."); + } + + /** + * Use reflection to grab the {@code defaults} field from a java.utils.Properties instance. + * + * @param parent The Properties to fetch default values from + * @return The Properties instance that was stored as defaults in the parent. + */ + protected Properties getDefaultFieldValue(Properties parent) { + Field field = ReflectionSupport.findFields(Properties.class, f -> f.getName().equals("defaults"), + HierarchyTraversalMode.BOTTOM_UP).stream().findFirst().get(); + + field.setAccessible(true); + + try { + + return (Properties) ReflectionSupport.tryToReadFieldValue(field, parent).get(); + + } + catch (Exception e) { + throw new RuntimeException("Unable to access the java.util.Properties.defaults field by reflection. " + + "Please adjust your local environment to allow this.", + e); + } + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java new file mode 100644 index 000000000000..6ef94f5bc2db --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java @@ -0,0 +1,749 @@ +/* + * 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.testkit.assertion; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Properties; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Verify proper behavior when annotated on a top level class. + * + *

These tests include testing object values but not object keys. {@code Properties} sort-of supports + * object values, but dies on common operations with object keys (e.g. {@code propertyNames()} fails), thus + * not testing.

+ * + *

Also, null keys and values are not allow in {@code Properties}.

+ * + */ +@DisplayName("PropertiesAssert Tests") +class PropertiesAssertTests { + + // Objects to put in Props + static final Object O_OBJ = new Object(); + static final Object P_OBJ = new Object(); + + static final Object Q_OBJ = new Object(); + static final Object R_OBJ = new Object(); + + // Two Properties objects w/ the exact same string contents + Properties strPropAB1; + Properties strPropAB2; + + // Same as propAandB but "B" comes from a default value + Properties strPropAB1CDwDefaults; + Properties strPropAB2CDwDefaults; + + Properties objProp1; + Properties objProp2; + + Properties objProp1wDefaults; + Properties objProp2wDefaults; + + @BeforeEach + void beforeEachMethod() { + strPropAB1 = new Properties(); + strPropAB1.setProperty("A", "is A"); + strPropAB1.setProperty("B", "is B"); + + strPropAB2 = new Properties(); + strPropAB2.setProperty("A", "is A"); + strPropAB2.setProperty("B", "is B"); + + strPropAB1CDwDefaults = new Properties(strPropAB1); + strPropAB1CDwDefaults.setProperty("C", "is C"); + strPropAB1CDwDefaults.setProperty("D", "is D"); + + strPropAB2CDwDefaults = new Properties(strPropAB2); + strPropAB2CDwDefaults.setProperty("C", "is C"); + strPropAB2CDwDefaults.setProperty("D", "is D"); + + objProp1 = new Properties(); + objProp1.put("O", O_OBJ); + objProp1.put("P", P_OBJ); + + objProp2 = new Properties(); + objProp2.put("O", O_OBJ); + objProp2.put("P", P_OBJ); + + objProp1wDefaults = new Properties(objProp1); + objProp1wDefaults.put("Q", Q_OBJ); + objProp1wDefaults.put("R", R_OBJ); + + objProp2wDefaults = new Properties(objProp2); + objProp2wDefaults.put("Q", Q_OBJ); + objProp2wDefaults.put("R", R_OBJ); + } + + @Test + void fakeTest() { + + } + + @Nested + @DisplayName("Top level String values") + class TopLevelStringValues { + + @Test + @DisplayName("Effectively and strictly same for identical") + void compareIdentical() { + new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); + new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + } + + @Test + @DisplayName("Not same for added actual value") + void addedActualValue() { + strPropAB1.setProperty("C", "I am not in set 2"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); + new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); + } + + @Test + @DisplayName("Not same added exp value") + void addedExpectedValue() { + strPropAB2.setProperty("C", "I am not in set 1"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); + new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); + } + + @Test + @DisplayName("Not same changed actual value") + void changedActualValue() { + strPropAB1.setProperty("B", "I am different"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); + new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); + } + + @Test + @DisplayName("Not same for changed exp value") + void changedExpectedValue() { + strPropAB2.setProperty("B", "I am different"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); + new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); + } + + @Test + @DisplayName("Not same for removed actual value") + void removedActualValue() { + strPropAB1.remove("B"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); + new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); + } + + @Test + @DisplayName("Not same for removed exp value") + void removedExpValue() { + strPropAB2.remove("B"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); + new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); + } + + } + + // This section is missing some tests that are in the above section + // Is not completely coverted over to objects (was copy paste from above) + @Nested + @DisplayName("Top level Object values") + class TopLevelObjectValues { + + @Test + @DisplayName("Effectively and strictly same for identical") + void compareIdentical() { + new PropertiesAssert(objProp1).isEffectivelyEqualsTo(objProp2); + new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + } + + @Test + @DisplayName("Not same for added actual value") + void addedActualValue() { + objProp1.put("Q", new Object()); + + new PropertiesAssert(objProp1).withFailMessage( + "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); + } + + @Test + @DisplayName("Not same added exp value") + void addedExpectedValue() { + objProp2.put("Q", new Object()); + + new PropertiesAssert(objProp1).withFailMessage( + "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); + } + + @Test + @DisplayName("Not same changed actual value") + void changedActualValue() { + objProp1.put("P", new Object()); + + new PropertiesAssert(objProp1).withFailMessage( + "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); + } + + @Test + @DisplayName("Not same for changed exp value") + void changedExpectedValue() { + objProp2.put("P", new Object()); + + new PropertiesAssert(objProp1).withFailMessage( + "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); + } + + @Test + @DisplayName("Not same for removed actual value") + void removedActualValue() { + objProp1.remove("P"); + + new PropertiesAssert(objProp1).withFailMessage( + "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); + } + + @Test + @DisplayName("Not same for removed exp value") + void removedExpValue() { + objProp2.remove("P"); + + new PropertiesAssert(objProp1).withFailMessage( + "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); + } + + } + + @Nested + @DisplayName("Nested default String values") + class NestedDefaultStringValues { + + @Test + @DisplayName("Effectively and strictly same for identical") + void compareIdentical() { + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + } + + @Test + @DisplayName("Not same for added actual default value") + void addedActualValue() { + strPropAB1.setProperty("E", "I am not in '2' and set in the default prop instance"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Not same added exp value") + void addedExpectedValue() { + strPropAB2.setProperty("E", "I am not in '1' and set in the default prop instance"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Not same changed actual value") + void changedActualValue() { + strPropAB1.setProperty("B", "I am different than '2' and set in the default prop instance"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Not same for changed exp value") + void changedExpectedValue() { + strPropAB2.setProperty("B", "I am different than '1' and set in the default prop instance"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Not same for removed actual value") + void removedActualValue() { + strPropAB1.remove("B"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Not same for removed exp value") + void removedExpValue() { + strPropAB2.remove("B"); + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Move actual value from default to top level") + void moveActualValueFromDefaultToTopLevel() { + strPropAB1CDwDefaults.put("B", strPropAB1.getProperty("B")); + strPropAB1.remove("B"); + + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Move actual value from top level to default") + void moveActualValueFromTopLevelToDefault() { + strPropAB1.put("D", strPropAB1CDwDefaults.getProperty("D")); + strPropAB1CDwDefaults.remove("D"); + + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Move exp value from default to top level") + void moveExpValueFromDefaultToTopLevel() { + strPropAB2CDwDefaults.put("B", strPropAB2.getProperty("B")); + strPropAB2.remove("B"); + + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + @Test + @DisplayName("Move exp value from top level to default") + void moveExpValueFromTopLevelToDefault() { + strPropAB2.put("D", strPropAB2CDwDefaults.getProperty("D")); + strPropAB2CDwDefaults.remove("D"); + + new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); + } + + } + + @Nested + @DisplayName("Nested default Object values") + class NestedDefaultObjectValues { + + @Test + @DisplayName("Effectively and strictly same for identical") + void compareIdentical() { + new PropertiesAssert(objProp1wDefaults).isEffectivelyEqualsTo(objProp2wDefaults); + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + } + + @Test + @DisplayName("Not same for added actual default value") + void addedActualValue() { + objProp1.put("X", new Object()); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Not same added exp value") + void addedExpectedValue() { + objProp2.put("X", new Object()); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Not same changed actual value") + void changedActualValue() { + objProp1.put("P", new Object()); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Not same for changed exp value") + void changedExpectedValue() { + objProp2.put("P", new Object()); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Not same for removed actual value") + void removedActualValue() { + objProp1.remove("P"); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Not same for removed exp value") + void removedExpValue() { + objProp2.remove("P"); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Move actual value from default to top level") + void moveActualValueFromDefaultToTopLevel() { + objProp1wDefaults.put("P", objProp1.get("P")); + objProp1.remove("P"); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Move actual value from top level to default") + void moveActualValueFromTopLevelToDefault() { + objProp1.put("R", objProp1wDefaults.get("R")); + objProp1wDefaults.remove("R"); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Move exp value from default to top level") + void moveExpValueFromDefaultToTopLevel() { + objProp2wDefaults.put("P", objProp2.get("P")); + objProp2.remove("P"); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + assertThatThrownBy(() -> { + new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); + }).isInstanceOf(AssertionError.class); + + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + @Test + @DisplayName("Move exp value from top level to default") + void moveExpValueFromTopLevelToDefault() { + objProp2.put("R", objProp2wDefaults.get("R")); + objProp2wDefaults.remove("R"); + + new PropertiesAssert(objProp1wDefaults).withFailMessage( + "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); + + assertThatThrownBy( + () -> new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults)).isInstanceOf( + AssertionError.class); + + assertThatThrownBy( + () -> new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults)).isInstanceOf( + AssertionError.class); + + new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); + } + + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java new file mode 100644 index 000000000000..245408fad32e --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java @@ -0,0 +1,86 @@ +/* + * 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.testkit.assertion; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.assertj.core.api.AbstractThrowableAssert; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.testkit.assertion.single.TestCaseAbortedAssert; +import org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert; +import org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.Events; + +class TestCaseAssertBase extends AbstractJUnitJupiterAssert + implements TestCaseStartedAssert, TestCaseFailureAssert, TestCaseAbortedAssert { + + TestCaseAssertBase(Events events) { + super(events, TestCaseAssertBase.class, 1); + } + + @Override + public AbstractThrowableAssert withExceptionInstanceOf(Class exceptionType) { + Throwable thrown = getRequiredThrowable(); + return assertThat(thrown).asInstanceOf(InstanceOfAssertFactories.throwable(exceptionType)); + } + + @Override + public AbstractThrowableAssert withException() { + Throwable thrown = getRequiredThrowable(); + return assertThat(thrown); + } + + @Override + public TestCaseFailureAssert whichFailed() { + assertThat(actual.failed().count()).isEqualTo(1); + return new TestCaseAssertBase(actual.failed()); + } + + @Override + public void whichSucceeded() { + assertThat(actual.succeeded().count()).isEqualTo(1); + } + + @Override + public TestCaseAbortedAssert whichAborted() { + assertThat(actual.aborted().count()).isEqualTo(1); + return new TestCaseAssertBase(actual.aborted()); + } + + @Override + public void withExceptionFulfilling(Predicate predicate) { + Throwable thrown = getRequiredThrowable(); + assertThat(predicate).accepts(thrown); + } + + @Override + public void andThenCheckException(Consumer testFunction) { + Throwable thrown = getRequiredThrowable(); + testFunction.accept(thrown); + } + + private Throwable getRequiredThrowable() { + Optional thrown = throwable(); + assertThat(thrown).isPresent(); + return thrown.get(); + } + + private Optional throwable() { + return actual.stream().findFirst().flatMap(fail -> fail.getPayload(TestExecutionResult.class)).flatMap( + TestExecutionResult::getThrowable); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java new file mode 100644 index 000000000000..18ab19277a09 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java @@ -0,0 +1,28 @@ +/* + * 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.testkit.assertion.single; + +import org.assertj.core.api.AbstractThrowableAssert; + +/** + * Used to assert a single aborted container or test. + */ +public interface TestCaseAbortedAssert { + + /** + * Asserts that the test/container was aborted because of a specific type of exception. + * + * @param exceptionType the expected type of the thrown exception + * @return an {@link AbstractThrowableAssert} for further assertions + */ + AbstractThrowableAssert withExceptionInstanceOf(Class exceptionType); + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java new file mode 100644 index 000000000000..c475183063ff --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java @@ -0,0 +1,90 @@ +/* + * 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.testkit.assertion.single; + +/** + * Assertions for asserting the state of single tests/containers. + */ +public interface TestCaseAssert { + + /** + * Asserts that there was exactly one started test. + * + * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions + */ + TestCaseStartedAssert hasSingleStartedTest(); + + /** + * Asserts that there was exactly one failed test. + * + * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert} for further assertions + */ + TestCaseFailureAssert hasSingleFailedTest(); + + /** + * Asserts that there was exactly one aborted test. + */ + void hasSingleAbortedTest(); + + /** + * Asserts that there was exactly one successful test. + */ + void hasSingleSucceededTest(); + + /** + * Asserts that there was exactly one skipped test. + */ + void hasSingleSkippedTest(); + + /** + * Asserts that there was exactly one dynamically registered test. + * + * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions + */ + TestCaseStartedAssert hasSingleDynamicallyRegisteredTest(); + + /** + * Asserts that there was exactly one started container. + * + * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions + */ + TestCaseStartedAssert hasSingleStartedContainer(); + + /** + * Asserts that there was exactly one failed container. + * + * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert} for further assertions + */ + TestCaseFailureAssert hasSingleFailedContainer(); + + /** + * Asserts that there was exactly one aborted container. + */ + void hasSingleAbortedContainer(); + + /** + * Asserts that there was exactly one succeeded container. + */ + void hasSingleSucceededContainer(); + + /** + * Asserts that there was exactly one skipped container. + */ + void hasSingleSkippedContainer(); + + /** + * Asserts that there was exactly one aborted container. + * + * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions + */ + TestCaseStartedAssert hasSingleDynamicallyRegisteredContainer(); + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java new file mode 100644 index 000000000000..86e26b393238 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java @@ -0,0 +1,52 @@ +/* + * 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.testkit.assertion.single; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.assertj.core.api.AbstractThrowableAssert; + +/** + * Used to assert a single failed container or test. + */ +public interface TestCaseFailureAssert { + + /** + * Asserts that the test/container failed because of a specific type of exception. + * + * @param exceptionType the expected type of the thrown exception + * @return an {@link AbstractThrowableAssert} for further assertions + */ + AbstractThrowableAssert withExceptionInstanceOf(Class exceptionType); + + /** + * Asserts that the test/container failed because an exception was thrown. + * + * @return an {@link AbstractThrowableAssert} for further assertions + */ + AbstractThrowableAssert withException(); + + /** + * Asserts that the test/container threw an exception that fulfills the supplied predicate. + * + * @param predicate the condition the thrown exception must fulfill + */ + void withExceptionFulfilling(Predicate predicate); + + /** + * Applies the supplied consumer to the exception thrown by the test/container. + * + * @param testFunction a consumer, for writing more flexible tests + */ + void andThenCheckException(Consumer testFunction); + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java new file mode 100644 index 000000000000..4471b99c2a9c --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java @@ -0,0 +1,38 @@ +/* + * 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.testkit.assertion.single; + +/** + * This interface contains methods for asserting a single test or container + * which was already asserted as "started". + */ +public interface TestCaseStartedAssert { + + /** + * Asserts that the test/container has succeeded. + */ + void whichSucceeded(); + + /** + * Asserts that the test/container was aborted. + * + * @return a {@link TestCaseAbortedAssert} for further assertions. + */ + TestCaseAbortedAssert whichAborted(); + + /** + * Asserts that the test/container has failed. + * + * @return a {@link TestCaseFailureAssert} for further assertions. + */ + TestCaseFailureAssert whichFailed(); + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java new file mode 100644 index 000000000000..ae71b8ab6cec --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java @@ -0,0 +1,17 @@ +/* + * 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.testkit.assertion.suite; + +/** + * Assertions for asserting multiple tests as part of a suite. + */ +public interface TestSuiteAssert extends TestSuiteTestsAssert, TestSuiteContainersAssert { +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java new file mode 100644 index 000000000000..a5ca5b0129d9 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java @@ -0,0 +1,66 @@ +/* + * 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.testkit.assertion.suite; + +public interface TestSuiteContainersAssert { + + /** + * Asserts that there were exactly {@code expected} number of started containers. + * + * @param expected the expected number of started containers + * @return a {@code TestSuiteContainersAssert} for further assertions. + */ + TestSuiteContainersAssert hasNumberOfStartedContainers(int expected); + + /** + * Asserts that there were exactly {@code expected} number of failed containers. + * + * @param expected the expected number of failed containers + * @return a {@code TestSuiteContainersFailureAssert} for further assertions. + */ + TestSuiteContainersFailureAssert hasNumberOfFailedContainers(int expected); + + /** + * Asserts that there were exactly {@code expected} number of aborted containers. + * + * @param expected the expected number of aborted containers + * @return a {@code TestSuiteContainersAssert} for further assertions. + */ + TestSuiteContainersAssert hasNumberOfAbortedContainers(int expected); + + /** + * Asserts that there were exactly {@code expected} number of succeeded containers. + * + * @param expected the expected number of succeeded containers + * @return a {@code TestSuiteContainersAssert} for further assertions. + */ + TestSuiteContainersAssert hasNumberOfSucceededContainers(int expected); + + /** + * Asserts that there were exactly {@code expected} number of skipped containers. + * + * @param expected the expected number of skipped containers + * @return a {@code TestSuiteContainersAssert} for further assertions. + */ + TestSuiteContainersAssert hasNumberOfSkippedContainers(int expected); + + /** + * Asserts that there were exactly {@code expected} number of dynamically registered containers. + * + * @param expected the expected number of dynamically registered containers + * @return a {@code TestSuiteContainersAssert} for further assertions. + */ + TestSuiteContainersAssert hasNumberOfDynamicallyRegisteredContainers(int expected); + + interface TestSuiteContainersFailureAssert extends TestSuiteContainersAssert, TestSuiteFailureAssert { + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java new file mode 100644 index 000000000000..64b46ef34f79 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java @@ -0,0 +1,55 @@ +/* + * 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.testkit.assertion.suite; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.assertj.core.api.ListAssert; + +interface TestSuiteFailureAssert { + + /** + * Asserts that all thrown exceptions are of a certain type. + * + * @param exceptionType the exception type you want to check + * @return a {@link ListAssert} for asserting exception messages + */ + /* + * Note: We avoid a varargs-variant of this method to prevent heap-pollution warnings. + * If you need a method with n exception types, create n-1 `withExceptionInstancesOf` + * (note the plural) overloads with [2, n] parameters. + */ + ListAssert withExceptionInstancesOf(Class exceptionType); + + /** + * Asserts that all failed tests failed because of a Throwable. + * + * @return a {@link ListAssert} for asserting exception messages + */ + ListAssert withExceptions(); + + /** + * Asserts that the thrown exceptions fulfill the condition of the given predicate. + * + * @param predicate the condition the exceptions must fulfill + */ + void assertingExceptions(Predicate> predicate); + + /** + * Applies the supplied consumer to the exceptions thrown by the tests/containers. + * + * @param testFunction a consumer, for writing more flexible tests + */ + void andThenCheckExceptions(Consumer> testFunction); + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java new file mode 100644 index 000000000000..986bf6b5767f --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java @@ -0,0 +1,66 @@ +/* + * 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.testkit.assertion.suite; + +public interface TestSuiteTestsAssert { + + /** + * Asserts that there were exactly {@code expected} number of started tests. + * + * @param expected the expected number of started tests + * @return a {@code TestSuiteTestsAssert} for further assertions. + */ + TestSuiteTestsAssert hasNumberOfStartedTests(int expected); + + /** + * Asserts that there were exactly {@code expected} number of failed tests. + * + * @param expected the expected number of failed tests + * @return a {@code TestSuiteTestsFailureAssert} for further assertions. + */ + TestSuiteTestsFailureAssert hasNumberOfFailedTests(int expected); + + /** + * Asserts that there were exactly {@code expected} number of aborted tests. + * + * @param expected the expected number of aborted tests + * @return a {@code TestSuiteTestsAssert} for further assertions. + */ + TestSuiteTestsAssert hasNumberOfAbortedTests(int expected); + + /** + * Asserts that there were exactly {@code expected} number of succeeded tests. + * + * @param expected the expected number of succeeded tests + * @return a {@code TestSuiteTestsAssert} for further assertions. + */ + TestSuiteTestsAssert hasNumberOfSucceededTests(int expected); + + /** + * Asserts that there were exactly {@code expected} number of skipped tests. + * + * @param expected the expected number of skipped tests + * @return a {@code TestSuiteTestsAssert} for further assertions. + */ + TestSuiteTestsAssert hasNumberOfSkippedTests(int expected); + + /** + * Asserts that there were exactly {@code expected} number of dynamically registered tests. + * + * @param expected the expected number of dynamically registered tests + * @return a {@code TestSuiteTestsAssert} for further assertions. + */ + TestSuiteTestsAssert hasNumberOfDynamicallyRegisteredTests(int expected); + + interface TestSuiteTestsFailureAssert extends TestSuiteTestsAssert, TestSuiteFailureAssert { + } + +} From cb100d77fe47a33aade2879d29f3d7bb9c9a940e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Sun, 9 Nov 2025 13:01:02 +0100 Subject: [PATCH 2/7] Feedback doc --- .../src/docs/asciidoc/user-guide/writing-tests.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 026beff1e3d9..c47a288fca2c 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -3827,8 +3827,8 @@ include::{testDir}/example/AutoCloseDemo.java[tags=user_guide_example] <2> `WebClient` implements `java.lang.AutoCloseable` which defines a `close()` method that will be invoked after each `@Test` method. -[[writing-tests-built-in-extensions-DefaultLocalAndTimezone]] -==== The @DefaultLocal and @DefaultTimezone Extensions +[[writing-tests-built-in-extensions-DefaultLocaleAndTimeZone]] +==== The @DefaultLocale and @DefaultTimeZone Extensions ---- include::{testDir}/example/DefaultLocaleTimezoneExtensionDemo.java[tags=default_locale_language] @@ -3840,14 +3840,14 @@ After the annotated element has been executed, the initial default value is rest ===== `@DefaultLocale` -The default `Locale` can be specified using an https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#forLanguageTag-java.lang.String-[IETF BCP 47 language tag string] +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 https://docs.oracle.com/javase/8/docs/api/java/util/Locale.Builder.html[Locale Builder] can create an instance with: +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 From 0e6efd857d11e426529027e41511ec113229804d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Sun, 9 Nov 2025 13:28:11 +0100 Subject: [PATCH 3/7] Delete Assertions --- .../assertion/AbstractJUnitJupiterAssert.java | 38 - .../assertion/ExecutionResultAssert.java | 21 - .../testkit/assertion/JUnitJupiterAssert.java | 45 -- .../JUnitJupiterExecutionResultAssert.java | 317 -------- .../assertion/JUnitJupiterPathAssert.java | 64 -- .../testkit/assertion/PropertiesAssert.java | 194 ----- .../assertion/PropertiesAssertTests.java | 749 ------------------ .../testkit/assertion/TestCaseAssertBase.java | 86 -- .../single/TestCaseAbortedAssert.java | 28 - .../assertion/single/TestCaseAssert.java | 90 --- .../single/TestCaseFailureAssert.java | 52 -- .../single/TestCaseStartedAssert.java | 38 - .../assertion/suite/TestSuiteAssert.java | 17 - .../suite/TestSuiteContainersAssert.java | 66 -- .../suite/TestSuiteFailureAssert.java | 55 -- .../assertion/suite/TestSuiteTestsAssert.java | 66 -- 16 files changed, 1926 deletions(-) delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java delete mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java deleted file mode 100644 index 36f1fe49223d..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/AbstractJUnitJupiterAssert.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.testkit.assertion; - -import org.assertj.core.api.AbstractAssert; - -/** - * A very basic extension of the AbstractAssert, used to add a quantity to assertions. - * By storing this value in a field we don't have to refer back to it every time. - * - *

Instead of assertThat(results).hasTests().thatStarted(3).thenFailed(3) we can write - * assertThat(results).hasNumberOfTests(3).thatStarted().andAllOfThemFailed().

- * - * @param the "self" type of this assertion class. Please read - * " - * Emulating 'self types' using Java Generics to simplify fluent API implementation" - * for more details. - * @param the type of the "actual" value. - */ -abstract class AbstractJUnitJupiterAssert, ACTUAL> - extends AbstractAssert { - - protected final int expected; - - protected AbstractJUnitJupiterAssert(ACTUAL actual, Class self, int expected) { - super(actual, self); - this.expected = expected; - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java deleted file mode 100644 index 301fda1707a8..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/ExecutionResultAssert.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.testkit.assertion; - -import org.junit.jupiter.testkit.assertion.reportentry.ReportEntryAssert; -import org.junit.jupiter.testkit.assertion.single.TestCaseAssert; -import org.junit.jupiter.testkit.assertion.suite.TestSuiteAssert; - -/** - * Base interface for all {@link org.junit.jupiter.testkit.ExecutionResults} assertions. - */ -public interface ExecutionResultAssert extends TestSuiteAssert, TestCaseAssert, ReportEntryAssert { -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java deleted file mode 100644 index 1f71dc0010a0..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterAssert.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.testkit.assertion; - -import java.nio.file.Path; -import java.util.Properties; - -import org.junit.jupiter.testkit.ExecutionResults; - -/** - * Entry point to all JUnit Pioneer assertions. - */ -public class JUnitJupiterAssert { - - private JUnitJupiterAssert() { - // private constructor to prevent instantiation - } - - public static ExecutionResultAssert assertThat(ExecutionResults actual) { - return new JUnitJupiterExecutionResultAssert(actual); - } - - public static JUnitJupiterPathAssert assertThat(Path actual) { - return new JUnitJupiterPathAssert(actual); - } - - /** - * Make an assertion on a {@link Properties} instance. - * - * @param actual The {@link Properties} instance the assertion is made with respect to - * @return Assertion instance - */ - public static PropertiesAssert assertThat(Properties actual) { - return new PropertiesAssert(actual); - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java deleted file mode 100644 index ebe974c07497..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterExecutionResultAssert.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * 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.testkit.assertion; - -import static java.util.stream.Collectors.toList; - -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import org.assertj.core.api.AbstractAssert; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ListAssert; -import org.junit.jupiter.testkit.ExecutionResults; -import org.junit.jupiter.testkit.assertion.reportentry.ReportEntryContentAssert; -import org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert; -import org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert; -import org.junit.jupiter.testkit.assertion.suite.TestSuiteAssert; -import org.junit.jupiter.testkit.assertion.suite.TestSuiteContainersAssert; -import org.junit.jupiter.testkit.assertion.suite.TestSuiteTestsAssert; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.reporting.ReportEntry; -import org.junit.platform.testkit.engine.Events; - -class JUnitJupiterExecutionResultAssert extends AbstractAssert - implements ExecutionResultAssert, TestSuiteAssert, TestSuiteTestsAssert.TestSuiteTestsFailureAssert, - TestSuiteContainersAssert.TestSuiteContainersFailureAssert { - - private boolean test = true; - - JUnitJupiterExecutionResultAssert(ExecutionResults actual) { - super(actual, JUnitJupiterExecutionResultAssert.class); - } - - @Override - public ReportEntryContentAssert hasNumberOfReportEntries(int expected) { - try { - List> entries = reportEntries(); - Assertions.assertThat(entries).hasSize(expected); - Integer[] ones = IntStream.generate(() -> 1).limit(expected).boxed().toArray(Integer[]::new); - Assertions.assertThat(entries).extracting(Map::size).containsExactly(ones); - - List> entryList = entries.stream().flatMap( - map -> map.entrySet().stream()).collect(toList()); - - return new ReportEntryAssertBase(entryList, expected); - } - catch (AssertionError error) { - getAllExceptions(actual.allEvents().reportingEntryPublished()).forEach(error::addSuppressed); - throw error; - } - } - - @Override - public ReportEntryContentAssert hasSingleReportEntry() { - return hasNumberOfReportEntries(1); - } - - @Override - public void hasNoReportEntries() { - try { - Assertions.assertThat(reportEntries()).isEmpty(); - } - catch (AssertionError error) { - getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); - throw error; - } - } - - @Override - public TestCaseStartedAssert hasSingleStartedTest() { - Events events = actual.testEvents(); - assertSingleTest(events.started()); - // Don't filter started() here; this would prevent further assertions on test outcome on - // returned assert because outcome is reported for the FINISHED event, not the STARTED one - return new TestCaseAssertBase(events); - } - - @Override - public TestCaseFailureAssert hasSingleFailedTest() { - return assertSingleTest(actual.testEvents().failed()); - } - - @Override - public void hasSingleAbortedTest() { - assertSingleTest(actual.testEvents().aborted()); - } - - @Override - public void hasSingleSucceededTest() { - assertSingleTest(actual.testEvents().succeeded()); - } - - @Override - public void hasSingleSkippedTest() { - assertSingleTest(actual.testEvents().skipped()); - } - - @Override - public TestCaseStartedAssert hasSingleDynamicallyRegisteredTest() { - Events events = actual.testEvents(); - assertSingleTest(events.dynamicallyRegistered()); - // Don't filter dynamicallyRegistered() here; this would prevent further assertions on test outcome on - // returned assert because outcome is reported for the FINISHED event, not the DYNAMIC_TEST_REGISTERED one - return new TestCaseAssertBase(events); - } - - private TestCaseAssertBase assertSingleTest(Events events) { - try { - Assertions.assertThat(events.count()).isEqualTo(1); - } - catch (AssertionError error) { - getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); - throw error; - } - return new TestCaseAssertBase(events); - } - - @Override - public TestCaseStartedAssert hasSingleStartedContainer() { - Events events = actual.containerEvents(); - assertSingleTest(events.started()); - // Don't filter started() here; this would prevent further assertions on container outcome on - // returned assert because outcome is reported for the FINISHED event, not the STARTED one - return new TestCaseAssertBase(events); - } - - @Override - public TestCaseFailureAssert hasSingleFailedContainer() { - return assertSingleContainer(actual.containerEvents().failed()); - } - - @Override - public void hasSingleAbortedContainer() { - assertSingleContainer(actual.containerEvents().aborted()); - } - - @Override - public void hasSingleSucceededContainer() { - assertSingleContainer(actual.containerEvents().succeeded()); - } - - @Override - public void hasSingleSkippedContainer() { - assertSingleContainer(actual.containerEvents().skipped()); - } - - @Override - public TestCaseStartedAssert hasSingleDynamicallyRegisteredContainer() { - Events events = actual.containerEvents(); - assertSingleContainer(events.dynamicallyRegistered()); - // Don't filter dynamicallyRegistered() here; this would prevent further assertions on container outcome on - // returned assert because outcome is reported for the FINISHED event, not the DYNAMIC_TEST_REGISTERED one - return new TestCaseAssertBase(events); - } - - private TestCaseAssertBase assertSingleContainer(Events events) { - try { - Assertions.assertThat(events.count()).isEqualTo(1); - } - catch (AssertionError error) { - getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); - throw error; - } - return new TestCaseAssertBase(events); - } - - @Override - public TestSuiteTestsAssert hasNumberOfStartedTests(int expected) { - return hasNumberOfSpecificTests(actual.testEvents().started().count(), expected); - } - - @Override - public TestSuiteTestsFailureAssert hasNumberOfFailedTests(int expected) { - this.test = true; - return hasNumberOfSpecificTests(actual.testEvents().failed().count(), expected); - } - - @Override - public TestSuiteTestsAssert hasNumberOfAbortedTests(int expected) { - return hasNumberOfSpecificTests(actual.testEvents().aborted().count(), expected); - } - - @Override - public TestSuiteTestsAssert hasNumberOfSucceededTests(int expected) { - return hasNumberOfSpecificTests(actual.testEvents().succeeded().count(), expected); - } - - @Override - public TestSuiteTestsAssert hasNumberOfSkippedTests(int expected) { - return hasNumberOfSpecificTests(actual.testEvents().skipped().count(), expected); - } - - @Override - public TestSuiteTestsAssert hasNumberOfDynamicallyRegisteredTests(int expected) { - return hasNumberOfSpecificTests(actual.testEvents().dynamicallyRegistered().count(), expected); - } - - private TestSuiteTestsFailureAssert hasNumberOfSpecificTests(long tests, int expected) { - try { - Assertions.assertThat(tests).isEqualTo(expected); - return this; - } - catch (AssertionError error) { - getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); - throw error; - } - } - - @Override - public TestSuiteContainersAssert hasNumberOfStartedContainers(int expected) { - return hasNumberOfSpecificContainers(actual.containerEvents().started().count(), expected); - } - - @Override - public TestSuiteContainersFailureAssert hasNumberOfFailedContainers(int expected) { - this.test = false; - return hasNumberOfSpecificContainers(actual.containerEvents().failed().count(), expected); - } - - @Override - public TestSuiteContainersAssert hasNumberOfAbortedContainers(int expected) { - return hasNumberOfSpecificContainers(actual.containerEvents().aborted().count(), expected); - } - - @Override - public TestSuiteContainersAssert hasNumberOfSucceededContainers(int expected) { - return hasNumberOfSpecificContainers(actual.containerEvents().succeeded().count(), expected); - } - - @Override - public TestSuiteContainersAssert hasNumberOfSkippedContainers(int expected) { - return hasNumberOfSpecificContainers(actual.containerEvents().skipped().count(), expected); - } - - @Override - public TestSuiteContainersAssert hasNumberOfDynamicallyRegisteredContainers(int expected) { - return hasNumberOfSpecificContainers(actual.containerEvents().dynamicallyRegistered().count(), expected); - } - - private TestSuiteContainersFailureAssert hasNumberOfSpecificContainers(long containers, int expected) { - try { - Assertions.assertThat(containers).isEqualTo(expected); - return this; - } - catch (AssertionError error) { - getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); - throw error; - } - } - - private List> reportEntries() { - return actual.allEvents().reportingEntryPublished().stream().map( - event -> event.getPayload(ReportEntry.class)).filter(Optional::isPresent).map(Optional::get).map( - ReportEntry::getKeyValuePairs).collect(toList()); - } - - @Override - public final ListAssert withExceptionInstancesOf(Class exceptionType) { - return assertExceptions(events -> { - Stream> classStream = getAllExceptions(events).map(Throwable::getClass); - Assertions.assertThat(classStream).containsOnly(exceptionType); - }); - } - - @Override - public ListAssert withExceptions() { - return assertExceptions( - events -> Assertions.assertThat(events.failed().count()).isEqualTo(getAllExceptions(events).count())); - } - - private ListAssert assertExceptions(Consumer assertion) { - try { - Events events = getProperEvents(); - assertion.accept(events); - return new ListAssert<>(getAllExceptions(events).map(Throwable::getMessage)); - } - catch (AssertionError error) { - getAllExceptions(actual.allEvents()).forEach(error::addSuppressed); - throw error; - } - } - - private Events getProperEvents() { - return this.test ? actual.testEvents() : actual.containerEvents(); - } - - static Stream getAllExceptions(Events events) { - return events.stream().map(fail -> fail.getPayload(TestExecutionResult.class)).filter(Optional::isPresent).map( - Optional::get).map(TestExecutionResult::getThrowable).filter(Optional::isPresent).map(Optional::get); - } - - @Override - public void assertingExceptions(Predicate> predicate) { - List thrownExceptions = getAllExceptions(getProperEvents()).collect(toList()); - Assertions.assertThat(predicate).accepts(thrownExceptions); - } - - @Override - public void andThenCheckExceptions(Consumer> testFunction) { - List thrownExceptions = getAllExceptions(getProperEvents()).collect(toList()); - testFunction.accept(thrownExceptions); - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java deleted file mode 100644 index c53fa73e7add..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/JUnitJupiterPathAssert.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.testkit.assertion; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; - -import org.assertj.core.api.PathAssert; - -public class JUnitJupiterPathAssert extends PathAssert { - - JUnitJupiterPathAssert(Path path) { - super(path); - } - - public JUnitJupiterPathAssert canReadAndWriteFile() { - isNotNull(); - - Path textFile; - try { - textFile = Files.createTempFile(actual, "some-text-file", ".txt"); - } - catch (IOException e1) { - throw failure("Cannot create a file"); - } - - String expectedText = "some-text"; - try { - Files.write(textFile, List.of(expectedText)); - } - catch (IOException e) { - throw failure("Cannot write to a file"); - } - - String actualText; - try { - actualText = new String(Files.readAllBytes(textFile), UTF_8).trim(); - } - catch (IOException e) { - throw failure("Cannot read from a file"); - } - - if (!Objects.equals(actualText, expectedText)) { - throw failureWithActualExpected(actualText, expectedText, "File expected to contain <%s>, but was <%s>", - expectedText, actualText); - } - - return this; - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java deleted file mode 100644 index 76685dbfc4e8..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssert.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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.testkit.assertion; - -import java.lang.reflect.Field; -import java.util.Properties; - -import org.assertj.core.api.AbstractAssert; -import org.junit.platform.commons.support.HierarchyTraversalMode; -import org.junit.platform.commons.support.ReflectionSupport; - -/** - * Allows comparison of {@link Properties} with optional awareness of their structure, - * rather than just treating them as Maps. Object values, which are marginally supported - * by {@code Properties}, are supported in assertions as much as possible. - */ -public class PropertiesAssert extends AbstractAssert { - - PropertiesAssert(Properties actual) { - super(actual, PropertiesAssert.class); - } - - /** - * Assert Properties has the same effective values as the passed instance, but not - * the same nested default structure. - * - *

Properties are considered effectively equal if they have the same property - * names returned by {@code Properties.propertyNames()} and the same values returned by - * {@code getProperty(name)}. Properties may come from the properties instance itself, - * or from a nested default instance, indiscriminately. - * - *

Properties partially supports object values, but return null for {@code getProperty(name)} - * when the value is a non-string. This assertion follows the same rules: Any non-String - * value is considered null for comparison purposes. - * - * @param expected The actual is expected to be effectively the same as this Properties - * @return Assertion instance - */ - public PropertiesAssert isEffectivelyEqualsTo(Properties expected) { - - // Compare values present in actual - actual.propertyNames().asIterator().forEachRemaining(k -> { - - String kStr = k.toString(); - - String actValue = actual.getProperty(kStr); - String expValue = expected.getProperty(kStr); - - if (actValue == null) { - if (expValue != null) { - // An object value is the only way to get a null from getProperty() - throw failure("For the property '<%s>', " - + "the actual value was an object but the expected the string '<%s>'.", - k, expValue); - } - } - else if (!actValue.equals(expValue)) { - throw failure("For the property '<%s>', the actual value was <%s> but <%s> was expected", k, actValue, - expValue); - } - }); - - // Compare values present in expected - Anything not matching must not have been present in actual - expected.propertyNames().asIterator().forEachRemaining(k -> { - - String kStr = k.toString(); - - String actValue = actual.getProperty(kStr); - String expValue = expected.getProperty(kStr); - - if (expValue == null) { - if (actValue != null) { - - // An object value is the only way to get a null from getProperty() - throw failure("For the property '<%s>', " - + "the actual value was the string '<%s>', but an object was expected.", - k, actValue); - } - } - else if (!expValue.equals(actValue)) { - throw failure("The property <%s> was expected to be <%s>, but was missing", k, expValue); - } - }); - - return this; - } - - /** - * The converse of isEffectivelyEqualTo. - * - * @param expected The actual is expected to NOT be effectively equal to this Properties - * @return Assertion instance - */ - public PropertiesAssert isNotEffectivelyEqualTo(Properties expected) { - try { - isEffectivelyEqualsTo(expected); - } - catch (AssertionError ae) { - return this; // Expected - } - - throw failure("The actual Properties should not be effectively equal to the expected one."); - } - - /** - * Compare values directly present in Properties and recursively into default Properties. - * - * @param expected The actual is expected to be strictly equal to this Properties - * @return Assertion instance - */ - public PropertiesAssert isStrictlyEqualTo(Properties expected) { - - // Compare values present in actual - actual.keySet().forEach(k -> { - if (!actual.get(k).equals(expected.get(k))) { - throw failure("For the property <%s> the actual value was <%s> but <%s> was expected", k, actual.get(k), - expected.get(k)); - } - }); - - // Compare values present in expected - Anything not matching must not have been present in actual - expected.keySet().forEach(k -> { - if (!expected.get(k).equals(actual.get(k))) { - throw failure("The property <%s> was expected to be <%s>, but was missing", k, expected.get(k)); - } - }); - - // Dig down into the nested defaults - Properties actualDefault = getDefaultFieldValue(actual); - Properties expectedDefault = getDefaultFieldValue(expected); - - if (actualDefault != null && expectedDefault != null) { - return new PropertiesAssert(actualDefault).isStrictlyEqualTo(expectedDefault); - } - else if (actualDefault != null) { - throw failure("The actual Properties had non-null defaults, but none were expected"); - } - else if (expectedDefault != null) { - throw failure("The expected Properties had non-null defaults, but none were in actual"); - } - - return this; - } - - /** - * Simple converse of isStrictlyEqualTo. - * - * @param expected The actual is expected to NOT be strictly equal to this Properties - * @return Assertion instance - */ - public PropertiesAssert isNotStrictlyEqualTo(Properties expected) { - try { - isStrictlyEqualTo(expected); - } - catch (AssertionError ae) { - return this; // Expected - } - - throw failure("The actual Properties should not be strictly the same as the expected one."); - } - - /** - * Use reflection to grab the {@code defaults} field from a java.utils.Properties instance. - * - * @param parent The Properties to fetch default values from - * @return The Properties instance that was stored as defaults in the parent. - */ - protected Properties getDefaultFieldValue(Properties parent) { - Field field = ReflectionSupport.findFields(Properties.class, f -> f.getName().equals("defaults"), - HierarchyTraversalMode.BOTTOM_UP).stream().findFirst().get(); - - field.setAccessible(true); - - try { - - return (Properties) ReflectionSupport.tryToReadFieldValue(field, parent).get(); - - } - catch (Exception e) { - throw new RuntimeException("Unable to access the java.util.Properties.defaults field by reflection. " - + "Please adjust your local environment to allow this.", - e); - } - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java deleted file mode 100644 index 6ef94f5bc2db..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/PropertiesAssertTests.java +++ /dev/null @@ -1,749 +0,0 @@ -/* - * 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.testkit.assertion; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.Properties; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * Verify proper behavior when annotated on a top level class. - * - *

These tests include testing object values but not object keys. {@code Properties} sort-of supports - * object values, but dies on common operations with object keys (e.g. {@code propertyNames()} fails), thus - * not testing.

- * - *

Also, null keys and values are not allow in {@code Properties}.

- * - */ -@DisplayName("PropertiesAssert Tests") -class PropertiesAssertTests { - - // Objects to put in Props - static final Object O_OBJ = new Object(); - static final Object P_OBJ = new Object(); - - static final Object Q_OBJ = new Object(); - static final Object R_OBJ = new Object(); - - // Two Properties objects w/ the exact same string contents - Properties strPropAB1; - Properties strPropAB2; - - // Same as propAandB but "B" comes from a default value - Properties strPropAB1CDwDefaults; - Properties strPropAB2CDwDefaults; - - Properties objProp1; - Properties objProp2; - - Properties objProp1wDefaults; - Properties objProp2wDefaults; - - @BeforeEach - void beforeEachMethod() { - strPropAB1 = new Properties(); - strPropAB1.setProperty("A", "is A"); - strPropAB1.setProperty("B", "is B"); - - strPropAB2 = new Properties(); - strPropAB2.setProperty("A", "is A"); - strPropAB2.setProperty("B", "is B"); - - strPropAB1CDwDefaults = new Properties(strPropAB1); - strPropAB1CDwDefaults.setProperty("C", "is C"); - strPropAB1CDwDefaults.setProperty("D", "is D"); - - strPropAB2CDwDefaults = new Properties(strPropAB2); - strPropAB2CDwDefaults.setProperty("C", "is C"); - strPropAB2CDwDefaults.setProperty("D", "is D"); - - objProp1 = new Properties(); - objProp1.put("O", O_OBJ); - objProp1.put("P", P_OBJ); - - objProp2 = new Properties(); - objProp2.put("O", O_OBJ); - objProp2.put("P", P_OBJ); - - objProp1wDefaults = new Properties(objProp1); - objProp1wDefaults.put("Q", Q_OBJ); - objProp1wDefaults.put("R", R_OBJ); - - objProp2wDefaults = new Properties(objProp2); - objProp2wDefaults.put("Q", Q_OBJ); - objProp2wDefaults.put("R", R_OBJ); - } - - @Test - void fakeTest() { - - } - - @Nested - @DisplayName("Top level String values") - class TopLevelStringValues { - - @Test - @DisplayName("Effectively and strictly same for identical") - void compareIdentical() { - new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); - new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - } - - @Test - @DisplayName("Not same for added actual value") - void addedActualValue() { - strPropAB1.setProperty("C", "I am not in set 2"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); - new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); - } - - @Test - @DisplayName("Not same added exp value") - void addedExpectedValue() { - strPropAB2.setProperty("C", "I am not in set 1"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); - new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); - } - - @Test - @DisplayName("Not same changed actual value") - void changedActualValue() { - strPropAB1.setProperty("B", "I am different"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); - new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); - } - - @Test - @DisplayName("Not same for changed exp value") - void changedExpectedValue() { - strPropAB2.setProperty("B", "I am different"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); - new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); - } - - @Test - @DisplayName("Not same for removed actual value") - void removedActualValue() { - strPropAB1.remove("B"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); - new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); - } - - @Test - @DisplayName("Not same for removed exp value") - void removedExpValue() { - strPropAB2.remove("B"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isEffectivelyEqualsTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1).isStrictlyEqualTo(strPropAB2); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1).isNotEffectivelyEqualTo(strPropAB2); - new PropertiesAssert(strPropAB1).isNotStrictlyEqualTo(strPropAB2); - } - - } - - // This section is missing some tests that are in the above section - // Is not completely coverted over to objects (was copy paste from above) - @Nested - @DisplayName("Top level Object values") - class TopLevelObjectValues { - - @Test - @DisplayName("Effectively and strictly same for identical") - void compareIdentical() { - new PropertiesAssert(objProp1).isEffectivelyEqualsTo(objProp2); - new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - } - - @Test - @DisplayName("Not same for added actual value") - void addedActualValue() { - objProp1.put("Q", new Object()); - - new PropertiesAssert(objProp1).withFailMessage( - "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); - } - - @Test - @DisplayName("Not same added exp value") - void addedExpectedValue() { - objProp2.put("Q", new Object()); - - new PropertiesAssert(objProp1).withFailMessage( - "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); - } - - @Test - @DisplayName("Not same changed actual value") - void changedActualValue() { - objProp1.put("P", new Object()); - - new PropertiesAssert(objProp1).withFailMessage( - "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); - } - - @Test - @DisplayName("Not same for changed exp value") - void changedExpectedValue() { - objProp2.put("P", new Object()); - - new PropertiesAssert(objProp1).withFailMessage( - "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); - } - - @Test - @DisplayName("Not same for removed actual value") - void removedActualValue() { - objProp1.remove("P"); - - new PropertiesAssert(objProp1).withFailMessage( - "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); - } - - @Test - @DisplayName("Not same for removed exp value") - void removedExpValue() { - objProp2.remove("P"); - - new PropertiesAssert(objProp1).withFailMessage( - "Unable to see object value differences").isEffectivelyEqualsTo(objProp2); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isStrictlyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1).isNotEffectivelyEqualTo(objProp2); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1).isNotStrictlyEqualTo(objProp2); - } - - } - - @Nested - @DisplayName("Nested default String values") - class NestedDefaultStringValues { - - @Test - @DisplayName("Effectively and strictly same for identical") - void compareIdentical() { - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - } - - @Test - @DisplayName("Not same for added actual default value") - void addedActualValue() { - strPropAB1.setProperty("E", "I am not in '2' and set in the default prop instance"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Not same added exp value") - void addedExpectedValue() { - strPropAB2.setProperty("E", "I am not in '1' and set in the default prop instance"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Not same changed actual value") - void changedActualValue() { - strPropAB1.setProperty("B", "I am different than '2' and set in the default prop instance"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Not same for changed exp value") - void changedExpectedValue() { - strPropAB2.setProperty("B", "I am different than '1' and set in the default prop instance"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Not same for removed actual value") - void removedActualValue() { - strPropAB1.remove("B"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Not same for removed exp value") - void removedExpValue() { - strPropAB2.remove("B"); - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Move actual value from default to top level") - void moveActualValueFromDefaultToTopLevel() { - strPropAB1CDwDefaults.put("B", strPropAB1.getProperty("B")); - strPropAB1.remove("B"); - - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Move actual value from top level to default") - void moveActualValueFromTopLevelToDefault() { - strPropAB1.put("D", strPropAB1CDwDefaults.getProperty("D")); - strPropAB1CDwDefaults.remove("D"); - - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Move exp value from default to top level") - void moveExpValueFromDefaultToTopLevel() { - strPropAB2CDwDefaults.put("B", strPropAB2.getProperty("B")); - strPropAB2.remove("B"); - - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - @Test - @DisplayName("Move exp value from top level to default") - void moveExpValueFromTopLevelToDefault() { - strPropAB2.put("D", strPropAB2CDwDefaults.getProperty("D")); - strPropAB2CDwDefaults.remove("D"); - - new PropertiesAssert(strPropAB1CDwDefaults).isEffectivelyEqualsTo(strPropAB2CDwDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isStrictlyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(strPropAB1CDwDefaults).isNotEffectivelyEqualTo(strPropAB2CDwDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(strPropAB1CDwDefaults).isNotStrictlyEqualTo(strPropAB2CDwDefaults); - } - - } - - @Nested - @DisplayName("Nested default Object values") - class NestedDefaultObjectValues { - - @Test - @DisplayName("Effectively and strictly same for identical") - void compareIdentical() { - new PropertiesAssert(objProp1wDefaults).isEffectivelyEqualsTo(objProp2wDefaults); - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - } - - @Test - @DisplayName("Not same for added actual default value") - void addedActualValue() { - objProp1.put("X", new Object()); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Not same added exp value") - void addedExpectedValue() { - objProp2.put("X", new Object()); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Not same changed actual value") - void changedActualValue() { - objProp1.put("P", new Object()); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Not same for changed exp value") - void changedExpectedValue() { - objProp2.put("P", new Object()); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Not same for removed actual value") - void removedActualValue() { - objProp1.remove("P"); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Not same for removed exp value") - void removedExpValue() { - objProp2.remove("P"); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Move actual value from default to top level") - void moveActualValueFromDefaultToTopLevel() { - objProp1wDefaults.put("P", objProp1.get("P")); - objProp1.remove("P"); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Move actual value from top level to default") - void moveActualValueFromTopLevelToDefault() { - objProp1.put("R", objProp1wDefaults.get("R")); - objProp1wDefaults.remove("R"); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Move exp value from default to top level") - void moveExpValueFromDefaultToTopLevel() { - objProp2wDefaults.put("P", objProp2.get("P")); - objProp2.remove("P"); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - assertThatThrownBy(() -> { - new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults); - }).isInstanceOf(AssertionError.class); - - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - @Test - @DisplayName("Move exp value from top level to default") - void moveExpValueFromTopLevelToDefault() { - objProp2.put("R", objProp2wDefaults.get("R")); - objProp2wDefaults.remove("R"); - - new PropertiesAssert(objProp1wDefaults).withFailMessage( - "'Effective' should treat object values as null").isEffectivelyEqualsTo(objProp2wDefaults); - - assertThatThrownBy( - () -> new PropertiesAssert(objProp1wDefaults).isStrictlyEqualTo(objProp2wDefaults)).isInstanceOf( - AssertionError.class); - - assertThatThrownBy( - () -> new PropertiesAssert(objProp1wDefaults).isNotEffectivelyEqualTo(objProp2wDefaults)).isInstanceOf( - AssertionError.class); - - new PropertiesAssert(objProp1wDefaults).isNotStrictlyEqualTo(objProp2wDefaults); - } - - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java deleted file mode 100644 index 245408fad32e..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/TestCaseAssertBase.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.testkit.assertion; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import org.assertj.core.api.AbstractThrowableAssert; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.testkit.assertion.single.TestCaseAbortedAssert; -import org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert; -import org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.testkit.engine.Events; - -class TestCaseAssertBase extends AbstractJUnitJupiterAssert - implements TestCaseStartedAssert, TestCaseFailureAssert, TestCaseAbortedAssert { - - TestCaseAssertBase(Events events) { - super(events, TestCaseAssertBase.class, 1); - } - - @Override - public AbstractThrowableAssert withExceptionInstanceOf(Class exceptionType) { - Throwable thrown = getRequiredThrowable(); - return assertThat(thrown).asInstanceOf(InstanceOfAssertFactories.throwable(exceptionType)); - } - - @Override - public AbstractThrowableAssert withException() { - Throwable thrown = getRequiredThrowable(); - return assertThat(thrown); - } - - @Override - public TestCaseFailureAssert whichFailed() { - assertThat(actual.failed().count()).isEqualTo(1); - return new TestCaseAssertBase(actual.failed()); - } - - @Override - public void whichSucceeded() { - assertThat(actual.succeeded().count()).isEqualTo(1); - } - - @Override - public TestCaseAbortedAssert whichAborted() { - assertThat(actual.aborted().count()).isEqualTo(1); - return new TestCaseAssertBase(actual.aborted()); - } - - @Override - public void withExceptionFulfilling(Predicate predicate) { - Throwable thrown = getRequiredThrowable(); - assertThat(predicate).accepts(thrown); - } - - @Override - public void andThenCheckException(Consumer testFunction) { - Throwable thrown = getRequiredThrowable(); - testFunction.accept(thrown); - } - - private Throwable getRequiredThrowable() { - Optional thrown = throwable(); - assertThat(thrown).isPresent(); - return thrown.get(); - } - - private Optional throwable() { - return actual.stream().findFirst().flatMap(fail -> fail.getPayload(TestExecutionResult.class)).flatMap( - TestExecutionResult::getThrowable); - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java deleted file mode 100644 index 18ab19277a09..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAbortedAssert.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.testkit.assertion.single; - -import org.assertj.core.api.AbstractThrowableAssert; - -/** - * Used to assert a single aborted container or test. - */ -public interface TestCaseAbortedAssert { - - /** - * Asserts that the test/container was aborted because of a specific type of exception. - * - * @param exceptionType the expected type of the thrown exception - * @return an {@link AbstractThrowableAssert} for further assertions - */ - AbstractThrowableAssert withExceptionInstanceOf(Class exceptionType); - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java deleted file mode 100644 index c475183063ff..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseAssert.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.testkit.assertion.single; - -/** - * Assertions for asserting the state of single tests/containers. - */ -public interface TestCaseAssert { - - /** - * Asserts that there was exactly one started test. - * - * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions - */ - TestCaseStartedAssert hasSingleStartedTest(); - - /** - * Asserts that there was exactly one failed test. - * - * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert} for further assertions - */ - TestCaseFailureAssert hasSingleFailedTest(); - - /** - * Asserts that there was exactly one aborted test. - */ - void hasSingleAbortedTest(); - - /** - * Asserts that there was exactly one successful test. - */ - void hasSingleSucceededTest(); - - /** - * Asserts that there was exactly one skipped test. - */ - void hasSingleSkippedTest(); - - /** - * Asserts that there was exactly one dynamically registered test. - * - * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions - */ - TestCaseStartedAssert hasSingleDynamicallyRegisteredTest(); - - /** - * Asserts that there was exactly one started container. - * - * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions - */ - TestCaseStartedAssert hasSingleStartedContainer(); - - /** - * Asserts that there was exactly one failed container. - * - * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseFailureAssert} for further assertions - */ - TestCaseFailureAssert hasSingleFailedContainer(); - - /** - * Asserts that there was exactly one aborted container. - */ - void hasSingleAbortedContainer(); - - /** - * Asserts that there was exactly one succeeded container. - */ - void hasSingleSucceededContainer(); - - /** - * Asserts that there was exactly one skipped container. - */ - void hasSingleSkippedContainer(); - - /** - * Asserts that there was exactly one aborted container. - * - * @return a {@link org.junit.jupiter.testkit.assertion.single.TestCaseStartedAssert} for further assertions - */ - TestCaseStartedAssert hasSingleDynamicallyRegisteredContainer(); - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java deleted file mode 100644 index 86e26b393238..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseFailureAssert.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.testkit.assertion.single; - -import java.util.function.Consumer; -import java.util.function.Predicate; - -import org.assertj.core.api.AbstractThrowableAssert; - -/** - * Used to assert a single failed container or test. - */ -public interface TestCaseFailureAssert { - - /** - * Asserts that the test/container failed because of a specific type of exception. - * - * @param exceptionType the expected type of the thrown exception - * @return an {@link AbstractThrowableAssert} for further assertions - */ - AbstractThrowableAssert withExceptionInstanceOf(Class exceptionType); - - /** - * Asserts that the test/container failed because an exception was thrown. - * - * @return an {@link AbstractThrowableAssert} for further assertions - */ - AbstractThrowableAssert withException(); - - /** - * Asserts that the test/container threw an exception that fulfills the supplied predicate. - * - * @param predicate the condition the thrown exception must fulfill - */ - void withExceptionFulfilling(Predicate predicate); - - /** - * Applies the supplied consumer to the exception thrown by the test/container. - * - * @param testFunction a consumer, for writing more flexible tests - */ - void andThenCheckException(Consumer testFunction); - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java deleted file mode 100644 index 4471b99c2a9c..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/single/TestCaseStartedAssert.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.testkit.assertion.single; - -/** - * This interface contains methods for asserting a single test or container - * which was already asserted as "started". - */ -public interface TestCaseStartedAssert { - - /** - * Asserts that the test/container has succeeded. - */ - void whichSucceeded(); - - /** - * Asserts that the test/container was aborted. - * - * @return a {@link TestCaseAbortedAssert} for further assertions. - */ - TestCaseAbortedAssert whichAborted(); - - /** - * Asserts that the test/container has failed. - * - * @return a {@link TestCaseFailureAssert} for further assertions. - */ - TestCaseFailureAssert whichFailed(); - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java deleted file mode 100644 index ae71b8ab6cec..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteAssert.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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.testkit.assertion.suite; - -/** - * Assertions for asserting multiple tests as part of a suite. - */ -public interface TestSuiteAssert extends TestSuiteTestsAssert, TestSuiteContainersAssert { -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java deleted file mode 100644 index a5ca5b0129d9..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteContainersAssert.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.testkit.assertion.suite; - -public interface TestSuiteContainersAssert { - - /** - * Asserts that there were exactly {@code expected} number of started containers. - * - * @param expected the expected number of started containers - * @return a {@code TestSuiteContainersAssert} for further assertions. - */ - TestSuiteContainersAssert hasNumberOfStartedContainers(int expected); - - /** - * Asserts that there were exactly {@code expected} number of failed containers. - * - * @param expected the expected number of failed containers - * @return a {@code TestSuiteContainersFailureAssert} for further assertions. - */ - TestSuiteContainersFailureAssert hasNumberOfFailedContainers(int expected); - - /** - * Asserts that there were exactly {@code expected} number of aborted containers. - * - * @param expected the expected number of aborted containers - * @return a {@code TestSuiteContainersAssert} for further assertions. - */ - TestSuiteContainersAssert hasNumberOfAbortedContainers(int expected); - - /** - * Asserts that there were exactly {@code expected} number of succeeded containers. - * - * @param expected the expected number of succeeded containers - * @return a {@code TestSuiteContainersAssert} for further assertions. - */ - TestSuiteContainersAssert hasNumberOfSucceededContainers(int expected); - - /** - * Asserts that there were exactly {@code expected} number of skipped containers. - * - * @param expected the expected number of skipped containers - * @return a {@code TestSuiteContainersAssert} for further assertions. - */ - TestSuiteContainersAssert hasNumberOfSkippedContainers(int expected); - - /** - * Asserts that there were exactly {@code expected} number of dynamically registered containers. - * - * @param expected the expected number of dynamically registered containers - * @return a {@code TestSuiteContainersAssert} for further assertions. - */ - TestSuiteContainersAssert hasNumberOfDynamicallyRegisteredContainers(int expected); - - interface TestSuiteContainersFailureAssert extends TestSuiteContainersAssert, TestSuiteFailureAssert { - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java deleted file mode 100644 index 64b46ef34f79..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteFailureAssert.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.testkit.assertion.suite; - -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import org.assertj.core.api.ListAssert; - -interface TestSuiteFailureAssert { - - /** - * Asserts that all thrown exceptions are of a certain type. - * - * @param exceptionType the exception type you want to check - * @return a {@link ListAssert} for asserting exception messages - */ - /* - * Note: We avoid a varargs-variant of this method to prevent heap-pollution warnings. - * If you need a method with n exception types, create n-1 `withExceptionInstancesOf` - * (note the plural) overloads with [2, n] parameters. - */ - ListAssert withExceptionInstancesOf(Class exceptionType); - - /** - * Asserts that all failed tests failed because of a Throwable. - * - * @return a {@link ListAssert} for asserting exception messages - */ - ListAssert withExceptions(); - - /** - * Asserts that the thrown exceptions fulfill the condition of the given predicate. - * - * @param predicate the condition the exceptions must fulfill - */ - void assertingExceptions(Predicate> predicate); - - /** - * Applies the supplied consumer to the exceptions thrown by the tests/containers. - * - * @param testFunction a consumer, for writing more flexible tests - */ - void andThenCheckExceptions(Consumer> testFunction); - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java deleted file mode 100644 index 986bf6b5767f..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/assertion/suite/TestSuiteTestsAssert.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.testkit.assertion.suite; - -public interface TestSuiteTestsAssert { - - /** - * Asserts that there were exactly {@code expected} number of started tests. - * - * @param expected the expected number of started tests - * @return a {@code TestSuiteTestsAssert} for further assertions. - */ - TestSuiteTestsAssert hasNumberOfStartedTests(int expected); - - /** - * Asserts that there were exactly {@code expected} number of failed tests. - * - * @param expected the expected number of failed tests - * @return a {@code TestSuiteTestsFailureAssert} for further assertions. - */ - TestSuiteTestsFailureAssert hasNumberOfFailedTests(int expected); - - /** - * Asserts that there were exactly {@code expected} number of aborted tests. - * - * @param expected the expected number of aborted tests - * @return a {@code TestSuiteTestsAssert} for further assertions. - */ - TestSuiteTestsAssert hasNumberOfAbortedTests(int expected); - - /** - * Asserts that there were exactly {@code expected} number of succeeded tests. - * - * @param expected the expected number of succeeded tests - * @return a {@code TestSuiteTestsAssert} for further assertions. - */ - TestSuiteTestsAssert hasNumberOfSucceededTests(int expected); - - /** - * Asserts that there were exactly {@code expected} number of skipped tests. - * - * @param expected the expected number of skipped tests - * @return a {@code TestSuiteTestsAssert} for further assertions. - */ - TestSuiteTestsAssert hasNumberOfSkippedTests(int expected); - - /** - * Asserts that there were exactly {@code expected} number of dynamically registered tests. - * - * @param expected the expected number of dynamically registered tests - * @return a {@code TestSuiteTestsAssert} for further assertions. - */ - TestSuiteTestsAssert hasNumberOfDynamicallyRegisteredTests(int expected); - - interface TestSuiteTestsFailureAssert extends TestSuiteTestsAssert, TestSuiteFailureAssert { - } - -} From a2c2dc18dac0c537894aa9db9b03f424ef6939d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Sun, 9 Nov 2025 13:28:24 +0100 Subject: [PATCH 4/7] Feedback Extensions and tests --- .../extension/DefaultTimeZoneExtension.java | 11 +-- .../engine/extension/DefaultLocaleTests.java | 70 ++++++++++--------- .../extension/DefaultTimeZoneTests.java | 38 +++++----- 3 files changed, 63 insertions(+), 56 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java index 004887e1f841..ab9da183ca3b 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTimeZoneExtension.java @@ -65,11 +65,12 @@ private static void validateCorrectConfiguration(DefaultTimeZone annotation) { private static TimeZone createTimeZone(String timeZoneId) { TimeZone configuredTimeZone = TimeZone.getTimeZone(timeZoneId); // TimeZone::getTimeZone returns with GMT as fallback if the given ID cannot be understood - if (configuredTimeZone.equals(TimeZone.getTimeZone("GMT")) && !timeZoneId.equals("GMT")) { - throw new ExtensionConfigurationException(String.format( - "@DefaultTimeZone not configured correctly. " + "Could not find the specified time zone + '%s'. " - + "Please use correct identifiers, e.g. \"GMT\" for Greenwich Mean Time.", - timeZoneId)); + if (configuredTimeZone.equals(TimeZone.getTimeZone("GMT")) && !"GMT".equals(timeZoneId)) { + throw new ExtensionConfigurationException(""" + @DefaultTimeZone not configured correctly. + Could not find the specified time zone + '%s'. + Please use correct identifiers, e.g. "GMT" for Greenwich Mean Time. + """.formatted(timeZoneId)); } return configuredTimeZone; } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java index ca264ee1161b..3a1bd21db3ee 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java @@ -13,7 +13,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestClass; import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestMethod; -import static org.junit.jupiter.testkit.assertion.JUnitJupiterAssert.assertThat; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; import java.util.Locale; @@ -108,7 +110,7 @@ void setsLanguageAndCountryAndVariant() { void shouldExecuteTestsWithConfiguredLocale() { ExecutionResults results = executeTestClass(ClassLevelTestCases.class); - assertThat(results).hasNumberOfSucceededTests(2); + results.testEvents().assertThatEvents().haveExactly(2, null); } @DefaultLocale(language = "fr", country = "FR") @@ -228,8 +230,8 @@ void shouldFailWhenNothingIsConfigured() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailMissingConfiguration"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @Test @@ -238,8 +240,8 @@ void shouldFailWhenVariantIsSetButCountryIsNot() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailMissingCountry"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @Test @@ -248,8 +250,8 @@ void shouldFailWhenLanguageTagAndLanguageIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailLanguageTagAndLanguage"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @Test @@ -258,8 +260,8 @@ void shouldFailWhenLanguageTagAndCountryIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailLanguageTagAndCountry"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @Test @@ -268,8 +270,8 @@ void shouldFailWhenLanguageTagAndVariantIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailLanguageTagAndVariant"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @Test @@ -278,8 +280,8 @@ void shouldFailIfNoValidBCP47VariantIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailNoValidBCP47Variant"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } } @@ -293,8 +295,8 @@ class ClassLevel { void shouldFailWhenVariantIsSetButCountryIsNot() { ExecutionResults results = executeTestClass(ClassLevelInitializationFailureTestCases.class); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } } @@ -378,8 +380,9 @@ void canUseProvider() { void providerReturnsNull() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "returnsNull"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - NullPointerException.class).hasMessageContaining("LocaleProvider instance returned with null"); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(NullPointerException.class), + message(it -> it.contains("LocaleProvider instance returned with null")))); } @Test @@ -388,9 +391,9 @@ void providerReturnsNull() { void mutuallyExclusiveWithValue() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithValue"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "can only be used with language tag if language, country, variant and provider are not set"); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains( + "can only be used with a provider if value, language, country and variant are not set.")))); } @Test @@ -399,9 +402,9 @@ void mutuallyExclusiveWithValue() { void mutuallyExclusiveWithLanguage() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithLanguage"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "can only be used with language tag if provider is not set"); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), + message(it -> it.contains("can only be used with language tag if provider is not set.")))); } @Test @@ -410,9 +413,9 @@ void mutuallyExclusiveWithLanguage() { void mutuallyExclusiveWithCountry() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithCountry"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "can only be used with a provider if value, language, country and variant are not set."); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains( + "can only be used with a provider if value, language, country and variant are not set.")))); } @Test @@ -421,9 +424,9 @@ void mutuallyExclusiveWithCountry() { void mutuallyExclusiveWithVariant() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithVariant"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "can only be used with a provider if value, language, country and variant are not set."); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains( + "can only be used with a provider if value, language, country and variant are not set.")))); } @Test @@ -432,9 +435,9 @@ void mutuallyExclusiveWithVariant() { void badConstructor() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "badConstructor"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "could not be constructed because of an exception"); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), + message(it -> it.contains("could not be constructed because of an exception")))); } } @@ -491,6 +494,7 @@ public Locale get() { static class ReturnsNullLocaleProvider implements LocaleProvider { @Override + @SuppressWarnings("NullAway") public Locale get() { return null; } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java index ac1beddb600a..f4861484c2b7 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java @@ -13,7 +13,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestClass; import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestMethod; -import static org.junit.jupiter.testkit.assertion.JUnitJupiterAssert.assertThat; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; import java.util.TimeZone; @@ -208,10 +210,9 @@ class ConfigurationTests { void throwsWhenConfigurationIsBad() { ExecutionResults results = executeTestMethod(BadMethodLevelConfigurationTestCases.class, "badConfiguration"); - - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageNotContaining( - "should never execute").hasMessageContaining("@DefaultTimeZone not configured correctly."); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), + message(it -> it.contains("@DefaultTimeZone not configured correctly.")))); } @Test @@ -220,9 +221,9 @@ void throwsWhenConfigurationIsBad() { void shouldThrowWithBadConfiguration() { ExecutionResults results = executeTestClass(BadClassLevelConfigurationTestCases.class); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "@DefaultTimeZone not configured correctly."); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), + message(it -> it.contains("@DefaultTimeZone not configured correctly.")))); } @AfterEach @@ -291,9 +292,9 @@ void defaultToGmt() { void throwsForMutuallyExclusiveOptions() { ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "notExclusive"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "Either a valid time zone id or a TimeZoneProvider must be provided"); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), + message(it -> it.contains("Either a valid time zone id or a TimeZoneProvider must be provided")))); } @Test @@ -302,9 +303,9 @@ void throwsForMutuallyExclusiveOptions() { void throwsForEmptyOptions() { ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "empty"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "Either a valid time zone id or a TimeZoneProvider must be provided"); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), + message(it -> it.contains("Either a valid time zone id or a TimeZoneProvider must be provided")))); } @Test @@ -313,9 +314,9 @@ void throwsForEmptyOptions() { void throwsForBadConstructor() { ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "noConstructor"); - assertThat(results).hasSingleFailedTest().withExceptionInstanceOf( - ExtensionConfigurationException.class).hasMessageContaining( - "Could not instantiate TimeZoneProvider because of exception"); + results.testEvents().assertThatEvents().haveExactly(1, + finishedWithFailure(instanceOf(ExtensionConfigurationException.class), + message(it -> it.contains("Could not instantiate TimeZoneProvider because of exception")))); } } @@ -354,6 +355,7 @@ public TimeZone get() { static class NullProvider implements TimeZoneProvider { @Override + @SuppressWarnings("NullAway") public TimeZone get() { return null; } @@ -364,7 +366,7 @@ static class ComplicatedProvider implements TimeZoneProvider { private final String timeZoneString; - public ComplicatedProvider(String timeZoneString) { + ComplicatedProvider(String timeZoneString) { this.timeZoneString = timeZoneString; } From 839b987887ec713b641327065c95f0ecb2f50028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Sun, 9 Nov 2025 13:34:10 +0100 Subject: [PATCH 5/7] apply ArchUnit rules --- .../main/java/org/junit/jupiter/api/util/DefaultLocale.java | 3 +++ .../main/java/org/junit/jupiter/api/util/DefaultTimeZone.java | 3 +++ .../main/java/org/junit/jupiter/api/util/LocaleProvider.java | 3 +++ .../java/org/junit/jupiter/api/util/ReadsDefaultLocale.java | 2 ++ .../java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java | 2 ++ .../main/java/org/junit/jupiter/api/util/TimeZoneProvider.java | 3 +++ .../java/org/junit/jupiter/api/util/WritesDefaultLocale.java | 2 ++ .../java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java | 2 ++ .../org/junit/jupiter/engine/support/JupiterLocaleUtils.java | 3 +++ 9 files changed, 23 insertions(+) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java index 9cc73236b95c..be73541af8cb 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultLocale.java @@ -16,6 +16,8 @@ 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. @@ -67,6 +69,7 @@ @Target({ ElementType.METHOD, ElementType.TYPE }) @Inherited @WritesDefaultLocale +@API(status = API.Status.STABLE, since = "6.1") public @interface DefaultLocale { /** diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java index 517598d1c779..1c1d87a9caa8 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/DefaultTimeZone.java @@ -16,6 +16,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; + /** * {@code @DefaultTimeZone} is a JUnit Jupiter extension to change the value * returned by {@link java.util.TimeZone#getDefault()} for a test execution. @@ -49,6 +51,7 @@ @Target({ ElementType.METHOD, ElementType.TYPE }) @Inherited @WritesDefaultTimeZone +@API(status = API.Status.STABLE, since = "6.1") public @interface DefaultTimeZone { /** diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java index d598385e9158..5916f482e307 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/LocaleProvider.java @@ -13,6 +13,9 @@ import java.util.Locale; import java.util.function.Supplier; +import org.apiguardian.api.API; + +@API(status = API.Status.STABLE, since = "6.1") public interface LocaleProvider extends Supplier { interface NullLocaleProvider extends LocaleProvider { diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java index 9d60abefd920..ff6b3d96de72 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultLocale.java @@ -16,6 +16,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; import org.junit.jupiter.api.parallel.ResourceAccessMode; import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.api.parallel.Resources; @@ -37,5 +38,6 @@ @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) @Inherited @ResourceLock(value = Resources.LOCALE, mode = ResourceAccessMode.READ) +@API(status = API.Status.STABLE, since = "6.1") public @interface ReadsDefaultLocale { } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java index de6f2642c3a8..9ecb223383ca 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/ReadsDefaultTimeZone.java @@ -16,6 +16,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; import org.junit.jupiter.api.parallel.ResourceAccessMode; import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.api.parallel.Resources; @@ -37,5 +38,6 @@ @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) @Inherited @ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ) +@API(status = API.Status.STABLE, since = "6.1") public @interface ReadsDefaultTimeZone { } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java index ed180dbcea75..8b876f2a80cf 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/TimeZoneProvider.java @@ -13,6 +13,9 @@ import java.util.TimeZone; import java.util.function.Supplier; +import org.apiguardian.api.API; + +@API(status = API.Status.STABLE, since = "6.1") public interface TimeZoneProvider extends Supplier { interface NullTimeZoneProvider extends TimeZoneProvider { diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java index ca0a1dc15020..8b692cb5d1cc 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultLocale.java @@ -16,6 +16,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; import org.junit.jupiter.api.parallel.ResourceAccessMode; import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.api.parallel.Resources; @@ -37,5 +38,6 @@ @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) @Inherited @ResourceLock(value = Resources.LOCALE, mode = ResourceAccessMode.READ_WRITE) +@API(status = API.Status.STABLE, since = "6.1") public @interface WritesDefaultLocale { } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java index 917ff1541d9b..93652c302b01 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/util/WritesDefaultTimeZone.java @@ -16,6 +16,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.apiguardian.api.API; import org.junit.jupiter.api.parallel.ResourceAccessMode; import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.api.parallel.Resources; @@ -37,5 +38,6 @@ @Target({ ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE }) @Inherited @ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ_WRITE) +@API(status = API.Status.STABLE, since = "6.1") public @interface WritesDefaultTimeZone { } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java index 6b651ed43799..4125cd250815 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/support/JupiterLocaleUtils.java @@ -12,9 +12,12 @@ import java.util.Locale; +import org.apiguardian.api.API; + /** * Utility class to create {@code Locale}. */ +@API(status = API.Status.INTERNAL, since = "6.1") public class JupiterLocaleUtils { private JupiterLocaleUtils() { From d8ff7d629c090f106166dc59807534fa88fd471d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Sun, 9 Nov 2025 13:59:21 +0100 Subject: [PATCH 6/7] Fix testengine conditions --- .../engine/extension/DefaultLocaleTests.java | 29 ++++++++++--------- .../extension/DefaultTimeZoneTests.java | 10 +++---- .../jupiter/testkit/JUnitJupiterKitTests.java | 12 ++++---- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java index 3a1bd21db3ee..6088cf092ff6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java @@ -13,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestClass; import static org.junit.jupiter.testkit.JUnitJupiterTestKit.executeTestMethod; +import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; @@ -110,7 +111,7 @@ void setsLanguageAndCountryAndVariant() { void shouldExecuteTestsWithConfiguredLocale() { ExecutionResults results = executeTestClass(ClassLevelTestCases.class); - results.testEvents().assertThatEvents().haveExactly(2, null); + results.testEvents().assertThatEvents().haveExactly(2, finishedSuccessfully()); } @DefaultLocale(language = "fr", country = "FR") @@ -230,7 +231,7 @@ void shouldFailWhenNothingIsConfigured() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailMissingConfiguration"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @@ -240,7 +241,7 @@ void shouldFailWhenVariantIsSetButCountryIsNot() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailMissingCountry"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @@ -250,7 +251,7 @@ void shouldFailWhenLanguageTagAndLanguageIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailLanguageTagAndLanguage"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @@ -260,7 +261,7 @@ void shouldFailWhenLanguageTagAndCountryIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailLanguageTagAndCountry"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @@ -270,7 +271,7 @@ void shouldFailWhenLanguageTagAndVariantIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailLanguageTagAndVariant"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @@ -280,7 +281,7 @@ void shouldFailIfNoValidBCP47VariantIsSet() { ExecutionResults results = executeTestMethod(MethodLevelInitializationFailureTestCases.class, "shouldFailNoValidBCP47Variant"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @@ -295,7 +296,7 @@ class ClassLevel { void shouldFailWhenVariantIsSetButCountryIsNot() { ExecutionResults results = executeTestClass(ClassLevelInitializationFailureTestCases.class); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class))); } @@ -380,7 +381,7 @@ void canUseProvider() { void providerReturnsNull() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "returnsNull"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(NullPointerException.class), message(it -> it.contains("LocaleProvider instance returned with null")))); } @@ -391,7 +392,7 @@ void providerReturnsNull() { void mutuallyExclusiveWithValue() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithValue"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains( "can only be used with a provider if value, language, country and variant are not set.")))); } @@ -402,7 +403,7 @@ void mutuallyExclusiveWithValue() { void mutuallyExclusiveWithLanguage() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithLanguage"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains("can only be used with language tag if provider is not set.")))); } @@ -413,7 +414,7 @@ void mutuallyExclusiveWithLanguage() { void mutuallyExclusiveWithCountry() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithCountry"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains( "can only be used with a provider if value, language, country and variant are not set.")))); } @@ -424,7 +425,7 @@ void mutuallyExclusiveWithCountry() { void mutuallyExclusiveWithVariant() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithVariant"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains( "can only be used with a provider if value, language, country and variant are not set.")))); } @@ -435,7 +436,7 @@ void mutuallyExclusiveWithVariant() { void badConstructor() { ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "badConstructor"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains("could not be constructed because of an exception")))); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java index f4861484c2b7..82e92cf6b8a1 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultTimeZoneTests.java @@ -210,7 +210,7 @@ class ConfigurationTests { void throwsWhenConfigurationIsBad() { ExecutionResults results = executeTestMethod(BadMethodLevelConfigurationTestCases.class, "badConfiguration"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains("@DefaultTimeZone not configured correctly.")))); } @@ -221,7 +221,7 @@ void throwsWhenConfigurationIsBad() { void shouldThrowWithBadConfiguration() { ExecutionResults results = executeTestClass(BadClassLevelConfigurationTestCases.class); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains("@DefaultTimeZone not configured correctly.")))); } @@ -292,7 +292,7 @@ void defaultToGmt() { void throwsForMutuallyExclusiveOptions() { ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "notExclusive"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains("Either a valid time zone id or a TimeZoneProvider must be provided")))); } @@ -303,7 +303,7 @@ void throwsForMutuallyExclusiveOptions() { void throwsForEmptyOptions() { ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "empty"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains("Either a valid time zone id or a TimeZoneProvider must be provided")))); } @@ -314,7 +314,7 @@ void throwsForEmptyOptions() { void throwsForBadConstructor() { ExecutionResults results = executeTestMethod(BadTimeZoneProviderTestCases.class, "noConstructor"); - results.testEvents().assertThatEvents().haveExactly(1, + results.testEvents().assertThatEvents().haveAtMost(1, finishedWithFailure(instanceOf(ExtensionConfigurationException.class), message(it -> it.contains("Could not instantiate TimeZoneProvider because of exception")))); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java index 9314160790a7..93b1a9e1e380 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/testkit/JUnitJupiterKitTests.java @@ -12,7 +12,7 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.testkit.assertion.JUnitJupiterAssert.assertThat; +import static org.junit.platform.testkit.engine.EventConditions.started; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -28,7 +28,7 @@ class JUnitJupiterKitTests { void executeTestClass() { ExecutionResults results = JUnitJupiterTestKit.executeTestClass(DummyClass.class); - assertThat(results).hasNumberOfStartedTests(1); + results.testEvents().assertThatEvents().haveExactly(1, started()); } @Test @@ -37,7 +37,7 @@ void executeTestClasses() { ExecutionResults results = JUnitJupiterTestKit.executeTestClasses( asList(DummyClass.class, SecondDummyClass.class)); - assertThat(results).hasNumberOfStartedTests(2); + results.testEvents().assertThatEvents().haveExactly(2, started()); } @Test @@ -45,7 +45,7 @@ void executeTestClasses() { void executeTestMethod() { ExecutionResults results = JUnitJupiterTestKit.executeTestMethod(DummyClass.class, "nothing"); - assertThat(results).hasNumberOfStartedTests(1); + results.testEvents().assertThatEvents().haveExactly(1, started()); } @Nested @@ -58,7 +58,7 @@ void executeTestMethodWithParameterTypes_singleParameterType() { ExecutionResults results = JUnitJupiterTestKit.executeTestMethodWithParameterTypes(DummyPropertyClass.class, "single", String.class); - assertThat(results).hasNumberOfStartedTests(1); + results.testEvents().assertThatEvents().haveExactly(1, started()); } @Test @@ -69,7 +69,7 @@ void executeTestMethodWithParameterTypes_parameterTypeAsArray() { ExecutionResults results = JUnitJupiterTestKit.executeTestMethodWithParameterTypes(DummyPropertyClass.class, "single", classes); - assertThat(results).hasNumberOfStartedTests(1); + results.testEvents().assertThatEvents().haveExactly(1, started()); } @Test From 7c9d76385506cb0024f47d2593a1381162946cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20B=C3=BCnger?= Date: Sun, 9 Nov 2025 14:06:05 +0100 Subject: [PATCH 7/7] Fix testengine conditions --- .../org/junit/jupiter/engine/extension/DefaultLocaleTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java index 6088cf092ff6..2718b4fc0b97 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/DefaultLocaleTests.java @@ -111,7 +111,7 @@ void setsLanguageAndCountryAndVariant() { void shouldExecuteTestsWithConfiguredLocale() { ExecutionResults results = executeTestClass(ClassLevelTestCases.class); - results.testEvents().assertThatEvents().haveExactly(2, finishedSuccessfully()); + results.testEvents().assertThatEvents().haveAtMost(2, finishedSuccessfully()); } @DefaultLocale(language = "fr", country = "FR")