diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 3806284591f1..58c016917350 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -137,10 +137,12 @@ endif::[] // Jupiter Extension APIs :extension-api-package: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/package-summary.html[org.junit.jupiter.api.extension] :AfterAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterAllCallback.html[AfterAllCallback] +:AfterContainerTemplateInvocationCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterContainerTemplateInvocationCallback.html[AfterContainerTemplateInvocationCallback] :AfterEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterEachCallback.html[AfterEachCallback] :AfterTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/AfterTestExecutionCallback.html[AfterTestExecutionCallback] :ParameterContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ParameterContext.html[ParameterContext] :BeforeAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeAllCallback.html[BeforeAllCallback] +:BeforeContainerTemplateInvocationCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeContainerTemplateInvocationCallback.html[BeforeContainerTemplateInvocationCallback] :BeforeEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeEachCallback.html[BeforeEachCallback] :BeforeTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.html[BeforeTestExecutionCallback] :ContainerTemplateInvocationContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.html[ContainerTemplateInvocationContext] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc index ce15d0bf27d8..7c400cb60757 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -68,6 +68,10 @@ repository on GitHub. updated documentation in the <<../user-guide/index.adoc#writing-tests-display-name-generator, User Guide>> for an example. +* New `BeforeContainerTemplateInvocationCallback` and + `AfterContainerTemplateInvocationCallback` extension callback interfaces allow + implementing extensions that are invoked before and after each invocation of a container + template. * New `TestTemplateInvocationContext.prepareInvocation(ExtensionContext)` callback method which allows extensions to prepare the `ExtensionContext` before the test template method is invoked. This may be used, for example, to store entries in the diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 65e7c77485a6..c8a664c66104 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -636,10 +636,14 @@ test execution lifecycle. Consult the following sections for examples and the Ja each of these interfaces in the `{extension-api-package}` package for further details. * `{BeforeAllCallback}` -** `{BeforeEachCallback}` -*** `{BeforeTestExecutionCallback}` -*** `{AfterTestExecutionCallback}` -** `{AfterEachCallback}` +** `{BeforeContainerTemplateInvocationCallback}` (only applicable for + <>) +*** `{BeforeEachCallback}` +**** `{BeforeTestExecutionCallback}` +**** `{AfterTestExecutionCallback}` +*** `{AfterEachCallback}` +** `{AfterContainerTemplateInvocationCallback}` (only applicable for + <>) * `{AfterAllCallback}` .Implementing Multiple Extension APIs @@ -1010,81 +1014,48 @@ image::extensions_lifecycle.png[caption='',title='{figure-caption}'] The following table further explains the sixteen steps in the <> diagram. -[cols="5,15,80"] -|=== -| Step | Interface/Annotation | Description - -| 1 -| interface `org.junit.jupiter.api.extension.BeforeAllCallback` -| extension code executed before all tests of the container are executed - -| 2 -| annotation `org.junit.jupiter.api.BeforeAll` -| user code executed before all tests of the container are executed - -| 3 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleBeforeAllMethodExecutionException` -| extension code for handling exceptions thrown from `@BeforeAll` methods - -| 4 -| interface `org.junit.jupiter.api.extension.BeforeEachCallback` -| extension code executed before each test is executed - -| 5 -| annotation `org.junit.jupiter.api.BeforeEach` -| user code executed before each test is executed - -| 6 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleBeforeEachMethodExecutionException` -| extension code for handling exceptions thrown from `@BeforeEach` methods - -| 7 -| interface `org.junit.jupiter.api.extension.BeforeTestExecutionCallback` -| extension code executed immediately before a test is executed - -| 8 -| annotation `org.junit.jupiter.api.Test` -| user code of the actual test method - -| 9 -| interface `org.junit.jupiter.api.extension.TestExecutionExceptionHandler` -| extension code for handling exceptions thrown during a test - -| 10 -| interface `org.junit.jupiter.api.extension.AfterTestExecutionCallback` -| extension code executed immediately after test execution and its corresponding exception handlers - -| 11 -| annotation `org.junit.jupiter.api.AfterEach` -| user code executed after each test is executed - -| 12 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleAfterEachMethodExecutionException` -| extension code for handling exceptions thrown from `@AfterEach` methods - -| 13 -| interface `org.junit.jupiter.api.extension.AfterEachCallback` -| extension code executed after each test is executed - -| 14 -| annotation `org.junit.jupiter.api.AfterAll` -| user code executed after all tests of the container are executed - -| 15 -| interface `org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler -#handleAfterAllMethodExecutionException` -| extension code for handling exceptions thrown from `@AfterAll` methods - -| 16 -| interface `org.junit.jupiter.api.extension.AfterAllCallback` -| extension code executed after all tests of the container are executed - -|=== - -In the simplest case only the actual test method will be executed (step 8); all other +. *interface* `*org.junit.jupiter.api.extension.BeforeAllCallback*` + +extension code executed before all tests of the container are executed +. *annotation* `*org.junit.jupiter.api.BeforeAll*` + +user code executed before all tests of the container are executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleBeforeAllMethodExecutionException*` + +extension code for handling exceptions thrown from `@BeforeAll` methods +. *interface* `*org.junit.jupiter.api.extension.BeforeContainerTemplateInvocationCallback*` + +extension code executed before each container template invocation is executed (only applicable if the test class is a <>) +. *interface* `*org.junit.jupiter.api.extension.BeforeEachCallback*` + +extension code executed before each test is executed +. *annotation* `*org.junit.jupiter.api.BeforeEach*` + +user code executed before each test is executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleBeforeEachMethodExecutionException*` + +extension code for handling exceptions thrown from `@BeforeEach` methods +. *interface* `*org.junit.jupiter.api.extension.BeforeTestExecutionCallback*` + +extension code executed immediately before a test is executed +. *annotation* `*org.junit.jupiter.api.Test*` + +user code of the actual test method +. *interface* `*org.junit.jupiter.api.extension.TestExecutionExceptionHandler*` + +extension code for handling exceptions thrown during a test +. *interface* `*org.junit.jupiter.api.extension.AfterTestExecutionCallback*` + +extension code executed immediately after test execution and its corresponding exception handlers +. *annotation* `*org.junit.jupiter.api.AfterEach*` + +user code executed after each test is executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleAfterEachMethodExecutionException*` + +extension code for handling exceptions thrown from `@AfterEach` methods +. *interface* `*org.junit.jupiter.api.extension.AfterEachCallback*` + +extension code executed after each test is executed +. *interface* `*org.junit.jupiter.api.extension.AfterContainerTemplateInvocationCallback*` + +extension code executed after each container template invocation is executed (only applicable if the test class is a <>) +. *annotation* `*org.junit.jupiter.api.AfterAll*` + +user code executed after all tests of the container are executed +. *interface* `*org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler +#handleAfterAllMethodExecutionException*` + +extension code for handling exceptions thrown from `@AfterAll` methods +. *interface* `*org.junit.jupiter.api.extension.AfterAllCallback*` + +extension code executed after all tests of the container are executed + +In the simplest case only the actual test method will be executed (step 9); all other steps are optional depending on the presence of user code or extension support for the corresponding lifecycle callback. For further details on the various lifecycle callbacks please consult the respective Javadoc for each annotation and extension. @@ -1097,6 +1068,7 @@ by implementing <> JUnit Jupiter always guarantees _wrapping_ behavior for multiple registered extensions that implement lifecycle callbacks such as `BeforeAllCallback`, `AfterAllCallback`, +`BeforeContainerTemplateInvocationCallback`, `AfterContainerTemplateInvocationCallback`, `BeforeEachCallback`, `AfterEachCallback`, `BeforeTestExecutionCallback`, and `AfterTestExecutionCallback`. diff --git a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png index bf8671b33f14..50d4f74e38c5 100644 Binary files a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png and b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle.png differ diff --git a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx index ed2d03f158c3..bc453b4e44e2 100644 Binary files a/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx and b/documentation/src/docs/asciidoc/user-guide/images/extensions_lifecycle_source.docx differ diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java index 6330e63c31d2..002e95d509e6 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java @@ -55,6 +55,8 @@ * @see TestTemplate * @see org.junit.jupiter.api.extension.ContainerTemplateInvocationContext * @see org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider + * @see org.junit.jupiter.api.extension.BeforeContainerTemplateInvocationCallback + * @see org.junit.jupiter.api.extension.AfterContainerTemplateInvocationCallback */ @Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java index 4c50c5f46c12..683bd3d6acfa 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterAllCallback.java @@ -34,8 +34,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -54,6 +56,8 @@ * @see AfterEachCallback * @see BeforeTestExecutionCallback * @see AfterTestExecutionCallback + * @see BeforeContainerTemplateInvocationCallback + * @see AfterContainerTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterContainerTemplateInvocationCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterContainerTemplateInvocationCallback.java new file mode 100644 index 000000000000..a43a4c80e445 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterContainerTemplateInvocationCallback.java @@ -0,0 +1,74 @@ +/* + * 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.extension; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * {@code AfterContainerTemplateInvocationCallback} defines the API for + * {@link Extension Extensions} that wish to provide additional behavior + * once before each invocation of a + * {@linkplain org.junit.jupiter.api.ContainerTemplate container template}. + * + *

Concrete implementations often implement + * {@link BeforeContainerTemplateInvocationCallback} + * as well. + * + *

Constructor Requirements

+ * + *

Consult the documentation in {@link Extension} for details on + * constructor requirements. + * + *

Wrapping Behavior

+ * + *

JUnit Jupiter guarantees wrapping behavior for multiple + * registered extensions that implement lifecycle callbacks such as + * {@link BeforeAllCallback}, {@link AfterAllCallback}, + * {@link AfterContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. + * + *

That means that, given two extensions {@code Extension1} and + * {@code Extension2} with {@code Extension1} registered before + * {@code Extension2}, any "before" callbacks implemented by {@code Extension1} + * are guaranteed to execute before any "before" callbacks implemented by + * {@code Extension2}. Similarly, given the two same two extensions registered + * in the same order, any "after" callbacks implemented by {@code Extension1} + * are guaranteed to execute after any "after" callbacks implemented by + * {@code Extension2}. {@code Extension1} is therefore said to wrap + * {@code Extension2}. + * + * @since 5.13 + * @see org.junit.jupiter.api.ContainerTemplate + * @see BeforeContainerTemplateInvocationCallback + * @see BeforeAllCallback + * @see AfterAllCallback + * @see BeforeEachCallback + * @see AfterEachCallback + * @see BeforeTestExecutionCallback + * @see AfterTestExecutionCallback + */ +@FunctionalInterface +@API(status = EXPERIMENTAL, since = "5.13") +public interface AfterContainerTemplateInvocationCallback extends Extension { + + /** + * Callback that is invoked after each invocation of a container + * template. + * + * @param context the current extension context; never {@code null} + */ + void afterContainerTemplateInvocation(ExtensionContext context) throws Exception; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java index a1be4e60e374..118613eb1dd1 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterEachCallback.java @@ -37,8 +37,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -57,6 +59,8 @@ * @see AfterTestExecutionCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeContainerTemplateInvocationCallback + * @see AfterContainerTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java index 367985a93914..385974cff6a6 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/AfterTestExecutionCallback.java @@ -38,8 +38,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -58,6 +60,8 @@ * @see AfterEachCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeContainerTemplateInvocationCallback + * @see AfterContainerTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java index d546e0da035f..04b61a4164c7 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeAllCallback.java @@ -34,8 +34,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -54,6 +56,8 @@ * @see AfterEachCallback * @see BeforeTestExecutionCallback * @see AfterTestExecutionCallback + * @see BeforeContainerTemplateInvocationCallback + * @see AfterContainerTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeContainerTemplateInvocationCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeContainerTemplateInvocationCallback.java new file mode 100644 index 000000000000..aa819cc848bf --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeContainerTemplateInvocationCallback.java @@ -0,0 +1,74 @@ +/* + * 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.extension; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * {@code BeforeContainerTemplateInvocationCallback} defines the API for + * {@link Extension Extensions} that wish to provide additional behavior + * once before each invocation of a + * {@linkplain org.junit.jupiter.api.ContainerTemplate container template}. + * + *

Concrete implementations often implement + * {@link AfterContainerTemplateInvocationCallback} + * as well. + * + *

Constructor Requirements

+ * + *

Consult the documentation in {@link Extension} for details on + * constructor requirements. + * + *

Wrapping Behavior

+ * + *

JUnit Jupiter guarantees wrapping behavior for multiple + * registered extensions that implement lifecycle callbacks such as + * {@link BeforeAllCallback}, {@link AfterAllCallback}, + * {@link BeforeContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. + * + *

That means that, given two extensions {@code Extension1} and + * {@code Extension2} with {@code Extension1} registered before + * {@code Extension2}, any "before" callbacks implemented by {@code Extension1} + * are guaranteed to execute before any "before" callbacks implemented by + * {@code Extension2}. Similarly, given the two same two extensions registered + * in the same order, any "after" callbacks implemented by {@code Extension1} + * are guaranteed to execute after any "after" callbacks implemented by + * {@code Extension2}. {@code Extension1} is therefore said to wrap + * {@code Extension2}. + * + * @since 5.13 + * @see org.junit.jupiter.api.ContainerTemplate + * @see AfterContainerTemplateInvocationCallback + * @see BeforeAllCallback + * @see AfterAllCallback + * @see BeforeEachCallback + * @see AfterEachCallback + * @see BeforeTestExecutionCallback + * @see AfterTestExecutionCallback + */ +@FunctionalInterface +@API(status = EXPERIMENTAL, since = "5.13") +public interface BeforeContainerTemplateInvocationCallback extends Extension { + + /** + * Callback that is invoked before each invocation of a container + * template. + * + * @param context the current extension context; never {@code null} + */ + void beforeContainerTemplateInvocation(ExtensionContext context) throws Exception; + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java index 6b23ad51df73..3a66bc0b840a 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeEachCallback.java @@ -37,8 +37,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -57,6 +59,8 @@ * @see AfterTestExecutionCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeContainerTemplateInvocationCallback + * @see AfterContainerTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java index 56094d6a133f..e79946cce20b 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.java @@ -38,8 +38,10 @@ *

JUnit Jupiter guarantees wrapping behavior for multiple * registered extensions that implement lifecycle callbacks such as * {@link BeforeAllCallback}, {@link AfterAllCallback}, - * {@link BeforeEachCallback}, {@link AfterEachCallback}, - * {@link BeforeTestExecutionCallback}, and {@link AfterTestExecutionCallback}. + * {@link BeforeContainerTemplateInvocationCallback}, + * {@link AfterContainerTemplateInvocationCallback}, {@link BeforeEachCallback}, + * {@link AfterEachCallback}, {@link BeforeTestExecutionCallback}, and + * {@link AfterTestExecutionCallback}. * *

That means that, given two extensions {@code Extension1} and * {@code Extension2} with {@code Extension1} registered before @@ -58,6 +60,8 @@ * @see AfterEachCallback * @see BeforeAllCallback * @see AfterAllCallback + * @see BeforeContainerTemplateInvocationCallback + * @see AfterContainerTemplateInvocationCallback */ @FunctionalInterface @API(status = STABLE, since = "5.0") diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/CallbackSupport.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/CallbackSupport.java new file mode 100644 index 000000000000..c7c6cf0e34bf --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/CallbackSupport.java @@ -0,0 +1,59 @@ +/* + * 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.descriptor; + +import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.engine.support.hierarchical.ThrowableCollector; + +/** + * @since 5.13 + */ +class CallbackSupport { + + static void invokeBeforeCallbacks(Class type, JupiterEngineExecutionContext context, + CallbackInvoker callbackInvoker) { + + ExtensionRegistry registry = context.getExtensionRegistry(); + ExtensionContext extensionContext = context.getExtensionContext(); + ThrowableCollector throwableCollector = context.getThrowableCollector(); + + for (T callback : registry.getExtensions(type)) { + throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext)); + if (throwableCollector.isNotEmpty()) { + break; + } + } + } + + static void invokeAfterCallbacks(Class type, JupiterEngineExecutionContext context, + CallbackInvoker callbackInvoker) { + + ExtensionRegistry registry = context.getExtensionRegistry(); + ExtensionContext extensionContext = context.getExtensionContext(); + ThrowableCollector throwableCollector = context.getThrowableCollector(); + + forEachInReverseOrder(registry.getExtensions(type), // + callback -> throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext))); + } + + @FunctionalInterface + protected interface CallbackInvoker { + + void invoke(T t, ExtensionContext context) throws Throwable; + + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index ea4f217a24d5..ca82d601a5ae 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -12,6 +12,8 @@ import static java.util.stream.Collectors.joining; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeAfterCallbacks; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeBeforeCallbacks; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters; @@ -23,7 +25,6 @@ import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findBeforeEachMethods; import static org.junit.jupiter.engine.descriptor.TestInstanceLifecycleUtils.getTestInstanceLifecycle; import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; -import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -406,16 +407,7 @@ private void executeAndMaskThrowable(Executable executable) { } private void invokeBeforeAllCallbacks(JupiterEngineExecutionContext context) { - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - for (BeforeAllCallback callback : registry.getExtensions(BeforeAllCallback.class)) { - throwableCollector.execute(() -> callback.beforeAll(extensionContext)); - if (throwableCollector.isNotEmpty()) { - break; - } - } + invokeBeforeCallbacks(BeforeAllCallback.class, context, BeforeAllCallback::beforeAll); } private void invokeBeforeAllMethods(JupiterEngineExecutionContext context) { @@ -472,20 +464,12 @@ private void invokeAfterAllMethodExecutionExceptionHandlers(ExtensionRegistry re } private void invokeAfterAllCallbacks(JupiterEngineExecutionContext context) { - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - forEachInReverseOrder(registry.getExtensions(AfterAllCallback.class), // - extension -> throwableCollector.execute(() -> extension.afterAll(extensionContext))); + invokeAfterCallbacks(AfterAllCallback.class, context, AfterAllCallback::afterAll); } private void invokeTestInstancePreDestroyCallbacks(JupiterEngineExecutionContext context) { - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - forEachInReverseOrder(context.getExtensionRegistry().getExtensions(TestInstancePreDestroyCallback.class), // - extension -> throwableCollector.execute(() -> extension.preDestroyTestInstance(extensionContext))); + invokeAfterCallbacks(TestInstancePreDestroyCallback.class, context, + TestInstancePreDestroyCallback::preDestroyTestInstance); } private boolean isPerClassLifecycle(JupiterEngineExecutionContext context) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java index 99cfdf2658d8..c6f70a419c14 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java @@ -11,6 +11,8 @@ package org.junit.jupiter.engine.descriptor; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeAfterCallbacks; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeBeforeCallbacks; import static org.junit.jupiter.engine.extension.MutableExtensionRegistry.createRegistryFrom; import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; @@ -21,6 +23,8 @@ import java.util.stream.Stream; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.AfterContainerTemplateInvocationCallback; +import org.junit.jupiter.api.extension.BeforeContainerTemplateInvocationCallback; import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; @@ -131,6 +135,14 @@ public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { return SkipResult.doNotSkip(); } + @Override + public JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) throws Exception { + invokeBeforeCallbacks(BeforeContainerTemplateInvocationCallback.class, context, + BeforeContainerTemplateInvocationCallback::beforeContainerTemplateInvocation); + context.getThrowableCollector().assertEmpty(); + return context; + } + @Override public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) throws Exception { @@ -139,6 +151,25 @@ public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext conte return context; } + @Override + public void after(JupiterEngineExecutionContext context) throws Exception { + + ThrowableCollector throwableCollector = context.getThrowableCollector(); + Throwable previousThrowable = throwableCollector.getThrowable(); + + invokeAfterCallbacks(AfterContainerTemplateInvocationCallback.class, context, + AfterContainerTemplateInvocationCallback::afterContainerTemplateInvocation); + + // If the previous Throwable was not null when this method was called, + // that means an exception was already thrown either before or during + // the execution of this Node. If an exception was already thrown, any + // later exceptions were added as suppressed exceptions to that original + // exception unless a more severe exception occurred in the meantime. + if (previousThrowable != throwableCollector.getThrowable()) { + throwableCollector.assertEmpty(); + } + } + @Override public void cleanUp(JupiterEngineExecutionContext context) throws Exception { // forget invocationContext so it can be garbage collected diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index d266e33e9cf6..8d6f961e96fa 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -11,10 +11,11 @@ package org.junit.jupiter.engine.descriptor; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeAfterCallbacks; +import static org.junit.jupiter.engine.descriptor.CallbackSupport.invokeBeforeCallbacks; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters; import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; -import static org.junit.platform.commons.util.CollectionUtils.forEachInReverseOrder; import java.lang.reflect.Method; import java.util.List; @@ -27,7 +28,6 @@ import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; -import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; @@ -182,21 +182,19 @@ private boolean isPerMethodLifecycle(JupiterEngineExecutionContext context) { } private void invokeBeforeEachCallbacks(JupiterEngineExecutionContext context) { - invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(BeforeEachCallback.class, context, - (callback, extensionContext) -> callback.beforeEach(extensionContext)); + invokeBeforeCallbacks(BeforeEachCallback.class, context, BeforeEachCallback::beforeEach); } private void invokeBeforeEachMethods(JupiterEngineExecutionContext context) { ExtensionRegistry registry = context.getExtensionRegistry(); - invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(BeforeEachMethodAdapter.class, context, - (adapter, extensionContext) -> { - try { - adapter.invokeBeforeEachMethod(extensionContext, registry); - } - catch (Throwable throwable) { - invokeBeforeEachExecutionExceptionHandlers(extensionContext, registry, throwable); - } - }); + invokeBeforeCallbacks(BeforeEachMethodAdapter.class, context, (adapter, extensionContext) -> { + try { + adapter.invokeBeforeEachMethod(extensionContext, registry); + } + catch (Throwable throwable) { + invokeBeforeEachExecutionExceptionHandlers(extensionContext, registry, throwable); + } + }); } private void invokeBeforeEachExecutionExceptionHandlers(ExtensionContext context, ExtensionRegistry registry, @@ -207,23 +205,8 @@ private void invokeBeforeEachExecutionExceptionHandlers(ExtensionContext context } private void invokeBeforeTestExecutionCallbacks(JupiterEngineExecutionContext context) { - invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(BeforeTestExecutionCallback.class, context, - (callback, extensionContext) -> callback.beforeTestExecution(extensionContext)); - } - - private void invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(Class type, - JupiterEngineExecutionContext context, CallbackInvoker callbackInvoker) { - - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - for (T callback : registry.getExtensions(type)) { - throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext)); - if (throwableCollector.isNotEmpty()) { - break; - } - } + invokeBeforeCallbacks(BeforeTestExecutionCallback.class, context, + BeforeTestExecutionCallback::beforeTestExecution); } protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) { @@ -252,13 +235,12 @@ private void invokeTestExecutionExceptionHandlers(ExtensionRegistry registry, Ex } private void invokeAfterTestExecutionCallbacks(JupiterEngineExecutionContext context) { - invokeAllAfterMethodsOrCallbacks(AfterTestExecutionCallback.class, context, - (callback, extensionContext) -> callback.afterTestExecution(extensionContext)); + invokeAfterCallbacks(AfterTestExecutionCallback.class, context, AfterTestExecutionCallback::afterTestExecution); } private void invokeAfterEachMethods(JupiterEngineExecutionContext context) { ExtensionRegistry registry = context.getExtensionRegistry(); - invokeAllAfterMethodsOrCallbacks(AfterEachMethodAdapter.class, context, (adapter, extensionContext) -> { + invokeAfterCallbacks(AfterEachMethodAdapter.class, context, (adapter, extensionContext) -> { try { adapter.invokeAfterEachMethod(extensionContext, registry); } @@ -276,27 +258,14 @@ private void invokeAfterEachExecutionExceptionHandlers(ExtensionContext context, } private void invokeAfterEachCallbacks(JupiterEngineExecutionContext context) { - invokeAllAfterMethodsOrCallbacks(AfterEachCallback.class, context, - (callback, extensionContext) -> callback.afterEach(extensionContext)); + invokeAfterCallbacks(AfterEachCallback.class, context, AfterEachCallback::afterEach); } private void invokeTestInstancePreDestroyCallbacks(JupiterEngineExecutionContext context) { - invokeAllAfterMethodsOrCallbacks(TestInstancePreDestroyCallback.class, context, + invokeAfterCallbacks(TestInstancePreDestroyCallback.class, context, TestInstancePreDestroyCallback::preDestroyTestInstance); } - private void invokeAllAfterMethodsOrCallbacks(Class type, - JupiterEngineExecutionContext context, CallbackInvoker callbackInvoker) { - - ExtensionRegistry registry = context.getExtensionRegistry(); - ExtensionContext extensionContext = context.getExtensionContext(); - ThrowableCollector throwableCollector = context.getThrowableCollector(); - - forEachInReverseOrder(registry.getExtensions(type), callback -> { - throwableCollector.execute(() -> callbackInvoker.invoke(callback, extensionContext)); - }); - } - /** * Invoke {@link TestWatcher#testSuccessful testSuccessful()}, * {@link TestWatcher#testAborted testAborted()}, or @@ -330,14 +299,4 @@ public void nodeFinished(JupiterEngineExecutionContext context, TestDescriptor d } } - /** - * @since 5.5 - */ - @FunctionalInterface - private interface CallbackInvoker { - - void invoke(T t, ExtensionContext context) throws Throwable; - - } - } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingBeforeEachCallback.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeContainerTemplateInvocationFieldInjector.java similarity index 68% rename from junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingBeforeEachCallback.java rename to junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeContainerTemplateInvocationFieldInjector.java index 7b65a9db3b44..34f5e215a1d9 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingBeforeEachCallback.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/BeforeContainerTemplateInvocationFieldInjector.java @@ -10,18 +10,18 @@ package org.junit.jupiter.params; -import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.BeforeContainerTemplateInvocationCallback; import org.junit.jupiter.api.extension.ExtensionContext; -class ContainerTemplateInstanceFieldInjectingBeforeEachCallback implements BeforeEachCallback { +class BeforeContainerTemplateInvocationFieldInjector implements BeforeContainerTemplateInvocationCallback { private final ResolverFacade resolverFacade; private final EvaluatedArgumentSet arguments; private final int invocationIndex; private final ResolutionCache resolutionCache; - ContainerTemplateInstanceFieldInjectingBeforeEachCallback(ResolverFacade resolverFacade, - EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { + BeforeContainerTemplateInvocationFieldInjector(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { this.resolverFacade = resolverFacade; this.arguments = arguments; this.invocationIndex = invocationIndex; @@ -29,7 +29,7 @@ class ContainerTemplateInstanceFieldInjectingBeforeEachCallback implements Befor } @Override - public void beforeEach(ExtensionContext extensionContext) throws Exception { + public void beforeContainerTemplateInvocation(ExtensionContext extensionContext) { extensionContext.getTestInstance() // .ifPresent(testInstance -> this.resolverFacade // .resolveAndInjectFields(testInstance, extensionContext, this.arguments, this.invocationIndex, diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingPostProcessor.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/InstancePostProcessingContainerTemplateFieldInjector.java similarity index 91% rename from junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingPostProcessor.java rename to junit-jupiter-params/src/main/java/org/junit/jupiter/params/InstancePostProcessingContainerTemplateFieldInjector.java index 9a2735d7bf59..3680948c9e7f 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingPostProcessor.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/InstancePostProcessingContainerTemplateFieldInjector.java @@ -13,14 +13,14 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstancePostProcessor; -class ContainerTemplateInstanceFieldInjectingPostProcessor implements TestInstancePostProcessor { +class InstancePostProcessingContainerTemplateFieldInjector implements TestInstancePostProcessor { private final ResolverFacade resolverFacade; private final EvaluatedArgumentSet arguments; private final int invocationIndex; private final ResolutionCache resolutionCache; - ContainerTemplateInstanceFieldInjectingPostProcessor(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, + InstancePostProcessingContainerTemplateFieldInjector(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { this.resolverFacade = resolverFacade; this.arguments = arguments; diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java index 5177aead98ee..76df5c9c5ce5 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java @@ -63,10 +63,10 @@ private Extension createExtensionForFieldInjection() { TestInstance.Lifecycle lifecycle = this.declarationContext.getTestInstanceLifecycle(); switch (lifecycle) { case PER_CLASS: - return new ContainerTemplateInstanceFieldInjectingBeforeEachCallback(resolverFacade, this.arguments, + return new BeforeContainerTemplateInvocationFieldInjector(resolverFacade, this.arguments, this.invocationIndex, this.resolutionCache); case PER_METHOD: - return new ContainerTemplateInstanceFieldInjectingPostProcessor(resolverFacade, this.arguments, + return new InstancePostProcessingContainerTemplateFieldInjector(resolverFacade, this.arguments, this.invocationIndex, this.resolutionCache); } throw new JUnitException("Unsupported lifecycle: " + lifecycle); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java index 73c0e3edc3d8..973b63892a5c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java @@ -35,11 +35,13 @@ public class KitchenSinkExtension implements // Lifecycle Callbacks BeforeAllCallback, - BeforeEachCallback, - BeforeTestExecutionCallback, - TestExecutionExceptionHandler, - AfterTestExecutionCallback, - AfterEachCallback, + BeforeContainerTemplateInvocationCallback, + BeforeEachCallback, + BeforeTestExecutionCallback, + TestExecutionExceptionHandler, + AfterTestExecutionCallback, + AfterEachCallback, + AfterContainerTemplateInvocationCallback, AfterAllCallback, // Lifecycle methods exception handling @@ -80,6 +82,10 @@ public ExtensionContextScope getTestInstantiationExtensionContextScope(Extension public void beforeAll(ExtensionContext context) { } + @Override + public void beforeContainerTemplateInvocation(ExtensionContext context) { + } + @Override public void beforeEach(ExtensionContext context) { } @@ -100,6 +106,10 @@ public void afterTestExecution(ExtensionContext context) { public void afterEach(ExtensionContext context) { } + @Override + public void afterContainerTemplateInvocation(ExtensionContext context) { + } + @Override public void afterAll(ExtensionContext context) { } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java index f56f63f191b3..0c09402d9304 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; @@ -39,10 +40,12 @@ import static org.junit.platform.testkit.engine.EventConditions.test; import static org.junit.platform.testkit.engine.EventConditions.uniqueId; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -54,13 +57,16 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.extension.AfterContainerTemplateInvocationCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeContainerTemplateInvocationCallback; import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; import org.junit.jupiter.api.extension.ExtendWith; @@ -71,6 +77,7 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.ContainerTemplateInvocationTestDescriptor; import org.junit.jupiter.engine.descriptor.ContainerTemplateTestDescriptor; @@ -85,6 +92,9 @@ import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.opentest4j.AssertionFailedError; +import org.opentest4j.TestAbortedException; /** * @since 5.13 @@ -846,49 +856,120 @@ void nestedContainerTemplateInvocationCanBeSelectedByIteration() { } @Test - void executesLifecycleCallbackMethodsInNestedContainerTemplates() { + void executesLifecycleCallbacksInNestedContainerTemplates() { var results = executeTestsForClass(TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase.class); results.containerEvents().assertStatistics(stats -> stats.started(10).succeeded(10)); - results.testEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8)); - var callSequence = results.allEvents().reportingEntryPublished() // - .map(event -> event.getRequiredPayload(ReportEntry.class)) // - .map(ReportEntry::getKeyValuePairs) // - .map(Map::values) // - .flatMap(Collection::stream); // @formatter:off - assertThat(callSequence).containsExactly( + assertThat(allReportEntryValues(results)).containsExactly( "beforeAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", - "beforeAll: NestedTestCase", - "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "beforeEach: test [NestedTestCase]", - "test", - "afterEach: test [NestedTestCase]", - "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "beforeEach: test [NestedTestCase]", - "test", - "afterEach: test [NestedTestCase]", - "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "afterAll: NestedTestCase", - "beforeAll: NestedTestCase", - "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "beforeEach: test [NestedTestCase]", - "test", - "afterEach: test [NestedTestCase]", - "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "beforeEach: test [NestedTestCase]", - "test", - "afterEach: test [NestedTestCase]", - "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", - "afterAll: NestedTestCase", + "beforeContainerTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeAll: NestedTestCase", + "beforeContainerTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterContainerTemplateInvocation: NestedTestCase", + "beforeContainerTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterContainerTemplateInvocation: NestedTestCase", + "afterAll: NestedTestCase", + "afterContainerTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeContainerTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeAll: NestedTestCase", + "beforeContainerTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterContainerTemplateInvocation: NestedTestCase", + "beforeContainerTemplateInvocation: NestedTestCase", + "beforeEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test1 [NestedTestCase]", + "test1", + "afterEach: test1 [NestedTestCase]", + "afterEach: test1 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test2 [NestedTestCase]", + "test2", + "afterEach: test2 [NestedTestCase]", + "afterEach: test2 [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterContainerTemplateInvocation: NestedTestCase", + "afterAll: NestedTestCase", + "afterContainerTemplateInvocation: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", "afterAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase" ); // @formatter:on } + @Test + void guaranteesWrappingBehaviorForCallbacks() { + var results = executeTestsForClass(CallbackWrappingBehaviorTestCase.class); + + results.containerEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + + // @formatter:off + assertThat(allReportEntryValues(results)).containsExactly( + "1st -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase", + "2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase", + "test", + "2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase", + "1st -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase", + "1st -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase", + "2nd -> beforeContainerTemplateInvocation: CallbackWrappingBehaviorTestCase", + "test", + "2nd -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase", + "1st -> afterContainerTemplateInvocation: CallbackWrappingBehaviorTestCase" + ); + // @formatter:on + } + + @Test + void propagatesExceptionsFromCallbacks() { + + var results = executeTestsForClass(CallbackExceptionBehaviorTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).failed(2).succeeded(2)); + + results.containerEvents().assertThatEvents() // + .haveExactly(2, finishedWithFailure( // + message("2nd -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase"), // + suppressed(0, + message("1st -> beforeContainerTemplateInvocation: CallbackExceptionBehaviorTestCase")), // + suppressed(1, + message("1st -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase")))); + + assertThat(allReportEntryValues(results).distinct()) // + .containsExactly("1st -> beforeContainerTemplateInvocation: CallbackExceptionBehaviorTestCase", // + "2nd -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase", // + "1st -> afterContainerTemplateInvocation: CallbackExceptionBehaviorTestCase"); + } + @Test void templateWithPreparations() { var results = executeTestsForClass(ContainerTemplateWithPreparationsTestCase.class); @@ -899,6 +980,14 @@ void templateWithPreparations() { // ------------------------------------------------------------------- + private static Stream allReportEntryValues(EngineExecutionResults results) { + return results.allEvents().reportingEntryPublished() // + .map(event -> event.getRequiredPayload(ReportEntry.class)) // + .map(ReportEntry::getKeyValuePairs) // + .map(Map::values) // + .flatMap(Collection::stream); + } + @SuppressWarnings("JUnitMalformedDeclaration") @ContainerTemplate @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) @@ -1227,15 +1316,24 @@ void test() { } @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + @ExtendWith(ContainerTemplateInvocationCallbacks.class) @ContainerTemplate static class TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase extends LifecycleCallbacks { + @Nested @ContainerTemplate class NestedTestCase extends LifecycleCallbacks { + @Test - @DisplayName("test") - void test(TestReporter testReporter) { - testReporter.publishEntry("test"); + @DisplayName("test1") + void test1(TestReporter testReporter) { + testReporter.publishEntry("test1"); + } + + @Test + @DisplayName("test2") + void test2(TestReporter testReporter) { + testReporter.publishEntry("test2"); } } } @@ -1341,4 +1439,86 @@ public void close() { } + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + @ContainerTemplate + static class CallbackWrappingBehaviorTestCase { + + @RegisterExtension + @Order(1) + static Extension first = new ContainerTemplateInvocationCallbacks("1st -> "); + + @RegisterExtension + @Order(2) + static Extension second = new ContainerTemplateInvocationCallbacks("2nd -> "); + + @Test + void test(TestReporter testReporter) { + testReporter.publishEntry("test"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + @ContainerTemplate + static class CallbackExceptionBehaviorTestCase { + + @RegisterExtension + @Order(1) + static Extension first = new ContainerTemplateInvocationCallbacks("1st -> ", TestAbortedException::new); + + @RegisterExtension + @Order(2) + static Extension second = new ContainerTemplateInvocationCallbacks("2nd -> ", AssertionFailedError::new); + + @Test + void test() { + fail("should not be called"); + } + } + + static class ContainerTemplateInvocationCallbacks + implements BeforeContainerTemplateInvocationCallback, AfterContainerTemplateInvocationCallback { + + private final String prefix; + private final Function exceptionFactory; + + @SuppressWarnings("unused") + ContainerTemplateInvocationCallbacks() { + this(""); + } + + ContainerTemplateInvocationCallbacks(String prefix) { + this(prefix, __ -> null); + } + + ContainerTemplateInvocationCallbacks(String prefix, Function exceptionFactory) { + this.prefix = prefix; + this.exceptionFactory = exceptionFactory; + } + + @Override + public void beforeContainerTemplateInvocation(ExtensionContext context) { + handle("beforeContainerTemplateInvocation", context); + } + + @Override + public void afterContainerTemplateInvocation(ExtensionContext context) { + handle("afterContainerTemplateInvocation", context); + } + + private void handle(String methodName, ExtensionContext context) { + var message = format(methodName, context); + context.publishReportEntry(message); + var throwable = exceptionFactory.apply(message); + if (throwable != null) { + throw throwAsUncheckedException(throwable); + } + } + + private String format(String methodName, ExtensionContext context) { + return "%s%s: %s".formatted(prefix, methodName, context.getRequiredTestClass().getSimpleName()); + } + } + }