Skip to content

Commit

Permalink
Add annotation for legacy and documentation
Browse files Browse the repository at this point in the history
Issue: #3445
  • Loading branch information
JojOatXGME committed Oct 5, 2024
1 parent 468d8cc commit 2744efb
Show file tree
Hide file tree
Showing 13 changed files with 425 additions and 57 deletions.
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ endif::[]
:BeforeAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeAllCallback.html[BeforeAllCallback]
: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]
:EnableTestScopedConstructorContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/EnableTestScopedConstructorContext.html[@EnableTestScopedConstructorContext]
:ExecutableInvoker: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutableInvoker.html[ExecutableInvoker]
:ExecutionCondition: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutionCondition.html[ExecutionCondition]
:ExtendWith: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ JUnit repository on GitHub.
[[release-notes-5.12.0-M1-junit-jupiter-deprecations-and-breaking-changes]]
==== Deprecations and Breaking Changes

* ❓
* `ParameterResolver` extensions receive a different `ExtensionContext` for constructor
parameters of the test instance. Since the `ExtensionContext` is now consistent with
parameters of test methods, extensions are unlikely to break, but the behavior may
change in certain scenarios.

[[release-notes-5.12.0-M1-junit-jupiter-new-features-and-improvements]]
==== New Features and Improvements
Expand All @@ -54,6 +57,12 @@ JUnit repository on GitHub.
`@ConvertWith`), and `ArgumentsAggregator` (declared via `@AggregateWith`)
implementations can now use constructor injection from registered `ParameterResolver`
extensions.
* Implementations of `ParameterResolver` now receive a test-specific `ExtensionContext`
for constructor parameters of the test class.
* `@EnableTestScopedConstructorContext` has been added to enable the use of a test-scoped
`ExtensionContext` in `TestInstancePreConstructCallback`, `TestInstancePostProcessor`
and `TestInstanceFactory`. The behavior enabled by the annotation is expected to
eventually become the default in future versions of JUnit Jupiter.


[[release-notes-5.12.0-M1-junit-vintage]]
Expand Down
18 changes: 18 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ This extension provides a symmetric call to `{TestInstancePreDestroyCallback}` a
in combination with other extensions to prepare constructor parameters or keeping track of test
instances and their lifecycle.

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
handling of `CloseableResource` and to make test-specific data available to your implementation.
====

[[extensions-test-instance-factories]]
=== Test Instance Factories

Expand All @@ -407,6 +413,12 @@ the user's responsibility to ensure that only a single `TestInstanceFactory` is
registered for any specific test class.
====

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
handling of `CloseableResource` and to make test-specific data available to your implementation.
====

[[extensions-test-instance-post-processing]]
=== Test Instance Post-processing

Expand All @@ -419,6 +431,12 @@ initialization methods on the test instance, etc.
For a concrete example, consult the source code for the `{MockitoExtension}` and the
`{SpringExtension}`.

[NOTE]
====
You may annotate your extension with `{EnableTestScopedConstructorContext}` for revised
handling of `CloseableResource` and to make test-specific data available to your implementation.
====

[[extensions-test-instance-pre-destroy-callback]]
=== Test Instance Pre-destroy Callback

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2015-2024 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.MAINTAINED;

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

import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code @EnableTestScopedConstructorContext} allows
* {@link Extension Extensions} to use a test-scoped {@link ExtensionContext}
* during creation of test instances.
*
* <p>The annotation should be used on extension classes.
* JUnit will call the following extension callbacks of annotated extensions
* with a test-scoped {@link ExtensionContext}, unless the test class is
* annotated with {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.
*
* <ul>
* <li>{@link TestInstancePreConstructCallback}</li>
* <li>{@link TestInstancePostProcessor}</li>
* <li>{@link TestInstanceFactory}</li>
* </ul>
*
* <p>Implementations of these extension callbacks can observe the following
* differences if they are using {@code @EnableTestScopedConstructorContext}.
*
* <ul>
* <li>{@link ExtensionContext#getElement() getElement()} may refer to the test
* method and {@link ExtensionContext#getTestClass() getTestClass()} may refer
* to a nested test class. Use {@link TestInstanceFactoryContext#getTestClass()}
* to get the class under construction.</li>
* <li>{@link ExtensionContext#getTestMethod() getTestMethod()} is no-longer
* empty, unless the test class is annotated with
* {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.</li>
* <li>If the callback adds a new {@link CloseableResource CloseableResource} to
* the {@link Store Store}, the resource is closed just after the instance is
* destroyed.</li>
* <li>The callbacks can now access data previously stored by
* {@link TestTemplateInvocationContext}, unless the test class is annotated
* with {@link TestInstance @TestInstance(Lifecycle.PER_CLASS)}.</li>
* </ul>
*
* <p><strong>Note</strong>: The behavior which is enabled by this annotation is
* expected to become the default in future versions of JUnit Jupiter. To ensure
* future compatibility, extension vendors are therefore advised to annotate
* their extensions, even if they don't need the new functionality.
*
* @since 5.12
* @see TestInstancePreConstructCallback
* @see TestInstancePostProcessor
* @see TestInstanceFactory
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@API(status = MAINTAINED, since = "5.12")
public @interface EnableTestScopedConstructorContext {
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.apiguardian.api.API.Status.STABLE;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code TestInstanceFactory} defines the API for {@link Extension
Expand Down Expand Up @@ -56,6 +57,11 @@ public interface TestInstanceFactory extends Extension {
/**
* Callback for creating a test instance for the supplied context.
*
* <p>You may annotate your extension with
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
* for revised handling of {@link CloseableResource CloseableResource} and
* to make test-specific data available to your implementation.
*
* <p><strong>Note</strong>: the {@code ExtensionContext} supplied to a
* {@code TestInstanceFactory} will always return an empty
* {@link java.util.Optional} value from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.apiguardian.api.API.Status.STABLE;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code TestInstancePostProcessor} defines the API for {@link Extension
Expand Down Expand Up @@ -45,6 +46,11 @@ public interface TestInstancePostProcessor extends Extension {
/**
* Callback for post-processing the supplied test instance.
*
* <p>You may annotate your extension with
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
* for revised handling of {@link CloseableResource CloseableResource} and
* to make test-specific data available to your implementation.
*
* <p><strong>Note</strong>: the {@code ExtensionContext} supplied to a
* {@code TestInstancePostProcessor} will always return an empty
* {@link java.util.Optional} value from {@link ExtensionContext#getTestInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;

/**
* {@code TestInstancePreConstructCallback} defines the API for {@link Extension
Expand Down Expand Up @@ -49,6 +50,11 @@ public interface TestInstancePreConstructCallback extends Extension {
/**
* Callback invoked prior to test instances being constructed.
*
* <p>You may annotate your extension with
* {@link EnableTestScopedConstructorContext @EnableTestScopedConstructorContext}
* for revised handling of {@link CloseableResource CloseableResource} and
* to make test-specific data available to your implementation.
*
* @param factoryContext the context for the test instance about to be instantiated;
* never {@code null}
* @param context the current extension context; never {@code null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.EnableTestScopedConstructorContext;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
Expand Down Expand Up @@ -66,6 +67,7 @@
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.jupiter.engine.extension.MutableExtensionRegistry;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.AnnotationUtils;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.commons.util.ReflectionUtils;
import org.junit.platform.commons.util.StringUtils;
Expand Down Expand Up @@ -277,15 +279,18 @@ private TestInstancesProvider testInstancesProvider(JupiterEngineExecutionContex
// For Lifecycle.PER_CLASS, ourExtensionContext.getTestInstances() is used to store the instance.
// Otherwise, extensionContext.getTestInstances() is always empty and we always create a new instance.
return (registry, context) -> ourExtensionContext.getTestInstances().orElseGet(
() -> instantiateAndPostProcessTestInstance(parentExecutionContext, registry, context));
() -> instantiateAndPostProcessTestInstance(parentExecutionContext, ourExtensionContext, registry,
context));
}

private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, JupiterEngineExecutionContext context) {
ClassExtensionContext ourExtensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context) {

TestInstances instances = instantiateTestClass(parentExecutionContext, registry, context);
TestInstances instances = instantiateTestClass(parentExecutionContext, ourExtensionContext, registry, context);
context.getThrowableCollector().execute(() -> {
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, context.getExtensionContext());
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, context.getExtensionContext(),
ourExtensionContext);
// In addition, we initialize extension registered programmatically from instance fields here
// since the best time to do that is immediately following test class instantiation
// and post-processing.
Expand All @@ -295,27 +300,35 @@ private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecuti
}

protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, JupiterEngineExecutionContext context);
ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context);

protected TestInstances instantiateTestClass(Optional<TestInstances> outerInstances, ExtensionRegistry registry,
ExtensionContext extensionContext) {
ExtensionContext extensionContext, ExtensionContext ourExtensionContext) {

Optional<Object> outerInstance = outerInstances.map(TestInstances::getInnermostInstance);
invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(this.testClass, outerInstance),
registry, extensionContext);
registry, extensionContext, ourExtensionContext);
Object instance = this.testInstanceFactory != null //
? invokeTestInstanceFactory(outerInstance, extensionContext) //
: invokeTestClassConstructor(outerInstance, registry, extensionContext);
? invokeTestInstanceFactory(outerInstance, extensionContext, ourExtensionContext) //
: invokeTestClassConstructor(outerInstance, registry, extensionContext, ourExtensionContext);
return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)).orElse(
DefaultTestInstances.of(instance));
}

private Object invokeTestInstanceFactory(Optional<Object> outerInstance, ExtensionContext extensionContext) {
private Object invokeTestInstanceFactory(Optional<Object> outerInstance, ExtensionContext extensionContext,
ExtensionContext ourExtensionContext) {
Object instance;

try {
instance = this.testInstanceFactory.createTestInstance(
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), extensionContext);
if (AnnotationUtils.isAnnotated(this.testInstanceFactory.getClass(),
EnableTestScopedConstructorContext.class)) {
instance = this.testInstanceFactory.createTestInstance(
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), extensionContext);
}
else {
instance = this.testInstanceFactory.createTestInstance(
new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), ourExtensionContext);
}
}
catch (Throwable throwable) {
UnrecoverableExceptions.rethrowIfUnrecoverable(throwable);
Expand Down Expand Up @@ -355,24 +368,36 @@ private Object invokeTestInstanceFactory(Optional<Object> outerInstance, Extensi
}

private Object invokeTestClassConstructor(Optional<Object> outerInstance, ExtensionRegistry registry,
ExtensionContext extensionContext) {
ExtensionContext extensionContext, ExtensionContext ourExtensionContext) {

Constructor<?> constructor = ReflectionUtils.getDeclaredConstructor(this.testClass);
return executableInvoker.invoke(constructor, outerInstance, extensionContext, registry,
InvocationInterceptor::interceptTestClassConstructor);
}

private void invokeTestInstancePreConstructCallbacks(TestInstanceFactoryContext factoryContext,
ExtensionRegistry registry, ExtensionContext context) {
registry.stream(TestInstancePreConstructCallback.class).forEach(
extension -> executeAndMaskThrowable(() -> extension.preConstructTestInstance(factoryContext, context)));
ExtensionRegistry registry, ExtensionContext context, ExtensionContext ourContext) {
registry.stream(TestInstancePreConstructCallback.class).forEach(extension -> {
if (AnnotationUtils.isAnnotated(extension.getClass(), EnableTestScopedConstructorContext.class)) {
executeAndMaskThrowable(() -> extension.preConstructTestInstance(factoryContext, context));
}
else {
executeAndMaskThrowable(() -> extension.preConstructTestInstance(factoryContext, ourContext));
}
});
}

private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry,
ExtensionContext context) {
private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry, ExtensionContext context,
ClassExtensionContext ourContext) {

registry.stream(TestInstancePostProcessor.class).forEach(
extension -> executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, context)));
registry.stream(TestInstancePostProcessor.class).forEach(extension -> {
if (AnnotationUtils.isAnnotated(extension.getClass(), EnableTestScopedConstructorContext.class)) {
executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, context));
}
else {
executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, ourContext));
}
});
}

private void executeAndMaskThrowable(Executable executable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Set;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
Expand Down Expand Up @@ -71,8 +72,8 @@ public ExecutionMode getExecutionMode() {

@Override
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, JupiterEngineExecutionContext context) {
return instantiateTestClass(Optional.empty(), registry, context.getExtensionContext());
ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context) {
return instantiateTestClass(Optional.empty(), registry, context.getExtensionContext(), ourExtensionContext);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Set;

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
Expand Down Expand Up @@ -74,13 +75,14 @@ public List<Class<?>> getEnclosingTestClasses() {

@Override
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, JupiterEngineExecutionContext context) {
ExtensionContext ourExtensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context) {

// Extensions registered for nested classes and below are not to be used for instantiating and initializing outer classes
ExtensionRegistry extensionRegistryForOuterInstanceCreation = parentExecutionContext.getExtensionRegistry();
TestInstances outerInstances = parentExecutionContext.getTestInstancesProvider().getTestInstances(
extensionRegistryForOuterInstanceCreation, context);
return instantiateTestClass(Optional.of(outerInstances), registry, context.getExtensionContext());
return instantiateTestClass(Optional.of(outerInstances), registry, context.getExtensionContext(),
ourExtensionContext);
}

}
Loading

0 comments on commit 2744efb

Please sign in to comment.