Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ExtensionContext on instance creation #4032

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
3 changes: 2 additions & 1 deletion documentation/src/test/java/example/TestInfoDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
class TestInfoDemo {

TestInfoDemo(TestInfo testInfo) {
assertEquals("TestInfo Demo", testInfo.getDisplayName());
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}

@BeforeEach
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")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure which status to use in the API annotation. Which status do you suggest?

If we want to us EXPERIMENTAL for now, then I should probably also reformulate my note in the Javadoc. (Suggesting to switch to an experimental API for future compatibility seems a bit odd. 😄)

public @interface EnableTestScopedConstructorContext {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to suggest other names. This is the best name that came to mind. I think the name is good enough, at least I like it much more than my other ideas (e.g. NewTestInstanceConstructionContext).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that you have already suggested @MethodLevelExtensionContextAware. I don't have a strong preference. Your suggestion is a bit shorter.

}
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 @@ -201,8 +203,7 @@ public JupiterEngineExecutionContext before(JupiterEngineExecutionContext contex
// and store the instance in the ExtensionContext.
ClassExtensionContext extensionContext = (ClassExtensionContext) context.getExtensionContext();
throwableCollector.execute(() -> {
TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(
context.getExtensionRegistry(), throwableCollector);
TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(context);
extensionContext.setTestInstances(testInstances);
});
}
Expand Down Expand Up @@ -273,52 +274,61 @@ private TestInstanceFactory resolveTestInstanceFactory(ExtensionRegistry registr
}

private TestInstancesProvider testInstancesProvider(JupiterEngineExecutionContext parentExecutionContext,
ClassExtensionContext extensionContext) {
ClassExtensionContext ourExtensionContext) {

return (registry, registrar, throwableCollector) -> extensionContext.getTestInstances().orElseGet(
() -> instantiateAndPostProcessTestInstance(parentExecutionContext, extensionContext, registry, registrar,
throwableCollector));
// 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, ourExtensionContext, registry,
context));
}

private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
ExtensionContext extensionContext, ExtensionRegistry registry, ExtensionRegistrar registrar,
ThrowableCollector throwableCollector) {
ClassExtensionContext ourExtensionContext, ExtensionRegistry registry,
JupiterEngineExecutionContext context) {

TestInstances instances = instantiateTestClass(parentExecutionContext, registry, registrar, extensionContext,
throwableCollector);
throwableCollector.execute(() -> {
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, extensionContext);
TestInstances instances = instantiateTestClass(parentExecutionContext, ourExtensionContext, registry, context);
context.getThrowableCollector().execute(() -> {
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.
registrar.initializeExtensions(this.testClass, instances.getInnermostInstance());
context.getExtensionRegistry().initializeExtensions(this.testClass, instances.getInnermostInstance());
});
return instances;
}

protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, ExtensionRegistrar registrar, ExtensionContext extensionContext,
ThrowableCollector throwableCollector);
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)) {
Comment on lines +323 to +324
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little bit concerned about performance with all these calls to AnnotationUtils.isAnnotated. Do you think this could be an issue?

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 @@ -358,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);
Comment on lines 370 to 375
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calls to ParameterResolver are not yet looking at the new annotation. Shall calls to ParameterResolver also consider the annotation?

I just wanted to confirm that I should adjust this method as well. I was a bit reluctant to change InterceptingExecutableInvoker for some reason. For now, I wrote the documentation matching the current behavior, I will change the documentation together with the implementation.

}

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 @@ -24,12 +24,10 @@
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.extension.ExtensionRegistrar;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestTag;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;

/**
* {@link TestDescriptor} for tests based on Java classes.
Expand Down Expand Up @@ -74,9 +72,8 @@ public ExecutionMode getExecutionMode() {

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

}
Loading