Skip to content

Commit

Permalink
Introduce ExtensionContext.getTestInstances() API
Browse files Browse the repository at this point in the history
The new `getTestInstances()` and `getRequiredTestInstances()` methods of
`ExtensionContext` allow accessing all test instances, including
enclosing ones for `@Nested` tests.

Resolves #1618.
  • Loading branch information
marcphilipp authored Dec 23, 2018
1 parent c804c80 commit f42aede
Show file tree
Hide file tree
Showing 21 changed files with 495 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ repository on GitHub.
methods via the `junit-jupiter-migrationsupport` module.
- See the <<../user-guide/index.adoc#migrating-from-junit4-ignore-annotation-support,
User Guide>> for details.
* New `ExtensionContext` methods to access all test instances, including enclosing ones
for `@Nested` tests: `getTestInstances()` and `getRequiredTestInstances()`.


[[release-notes-5.4.0-M1-junit-vintage]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;

import java.lang.reflect.AnnotatedElement;
Expand Down Expand Up @@ -160,6 +161,7 @@ default Class<?> getRequiredTestClass() {
* @return an {@code Optional} containing the test instance; never
* {@code null} but potentially empty
* @see #getRequiredTestInstance()
* @see #getTestInstances()
*/
Optional<Object> getTestInstance();

Expand All @@ -173,12 +175,49 @@ default Class<?> getRequiredTestClass() {
* @return the test instance; never {@code null}
* @throws PreconditionViolationException if the test instance is not present
* in this {@code ExtensionContext}
*
* @see #getRequiredTestInstances()
*/
default Object getRequiredTestInstance() {
return Preconditions.notNull(getTestInstance().orElse(null),
"Illegal state: required test instance is not present in the current ExtensionContext");
}

/**
* Get the test instances associated with the current test or container,
* if available.
*
* <p>While top-level tests only have a single test instance, nested tests
* have one additional instance for each enclosing test class.
*
* @return an {@code Optional} containing the test instances; never
* {@code null} but potentially empty
* @see #getRequiredTestInstances()
*
* @since 5.4
*/
@API(status = EXPERIMENTAL, since = "5.4")
Optional<TestInstances> getTestInstances();

/**
* Get the <em>required</em> test instances associated with the current test
* or container.
*
* <p>Use this method as an alternative to {@link #getTestInstances()} for use
* cases in which the test instances are required to be present.
*
* @return the test instances; never {@code null}
* @throws PreconditionViolationException if the test instances are not present
* in this {@code ExtensionContext}
*
* @since 5.4
*/
@API(status = EXPERIMENTAL, since = "5.4")
default TestInstances getRequiredTestInstances() {
return Preconditions.notNull(getTestInstances().orElse(null),
"Illegal state: required test instances are not present in the current ExtensionContext");
}

/**
* Get the {@link Method} associated with the current test, if available.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2015-2018 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
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.util.List;
import java.util.Optional;

import org.apiguardian.api.API;

/**
* {@code TestInstances} encapsulates the <em>test instances</em> of a test.
*
* <p>While top-level tests only have a single test instance, nested tests
* have one additional instance for each enclosing test class.
*
* @since 5.4
* @see ExtensionContext#getTestInstances()
* @see ExtensionContext#getRequiredTestInstances()
*/
@API(status = EXPERIMENTAL, since = "5.4")
public interface TestInstances {

/**
* Get the innermost test instance.
*
* <p>The innermost instance is the one closest to the test method.
*
* @return the innermost test instance; never {@code null}
*/
Object getInnermostInstance();

/**
* Get the enclosing test instances, excluding the innermost test instance,
* ordered from outermost to innermost.
*
* @return the enclosing test instances; never {@code null} or containing
* {@code null}, but potentially empty
*/
List<Object> getEnclosingInstances();

/**
* Get all test instances, ordered from outermost to innermost.
*
* @return all test instances; never {@code null}, containing {@code null},
* or empty
*/
List<Object> getAllInstances();

/**
* Find the first test instance that is an instance of the supplied required
* type, checking from innermost to outermost.
*
* @param requiredType the type to search for
* @return the first test instance of the required type; never {@code null}
* but potentially empty
*/
<T> Optional<T> findInstance(Class<T> requiredType);

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
Expand All @@ -33,7 +34,7 @@ public final class ClassExtensionContext extends AbstractExtensionContext<ClassT

private final ThrowableCollector throwableCollector;

private Object testInstance;
private TestInstances testInstances;

/**
* Create a new {@code ClassExtensionContext} with {@link Lifecycle#PER_METHOD}.
Expand Down Expand Up @@ -72,13 +73,18 @@ public Optional<Lifecycle> getTestInstanceLifecycle() {
return Optional.of(this.lifecycle);
}

void setTestInstance(Object testInstance) {
this.testInstance = testInstance;
@Override
public Optional<Object> getTestInstance() {
return getTestInstances().map(TestInstances::getInnermostInstance);
}

@Override
public Optional<Object> getTestInstance() {
return Optional.ofNullable(this.testInstance);
public Optional<TestInstances> getTestInstances() {
return Optional.ofNullable(testInstances);
}

void setTestInstances(TestInstances testInstances) {
this.testInstances = testInstances;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstanceFactory;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.api.extension.TestInstantiationException;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.AfterEachMethodAdapter;
import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter;
import org.junit.jupiter.engine.execution.DefaultTestInstances;
import org.junit.jupiter.engine.execution.ExecutableInvoker;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.execution.TestInstanceProvider;
import org.junit.jupiter.engine.execution.TestInstancesProvider;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.BlacklistedExceptions;
Expand Down Expand Up @@ -169,7 +171,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte

// @formatter:off
return context.extend()
.withTestInstanceProvider(testInstanceProvider(context, registry, extensionContext))
.withTestInstancesProvider(testInstancesProvider(context, registry, extensionContext))
.withExtensionRegistry(registry)
.withExtensionContext(extensionContext)
.withThrowableCollector(throwableCollector)
Expand All @@ -186,8 +188,8 @@ public JupiterEngineExecutionContext before(JupiterEngineExecutionContext contex
// Eagerly load test instance for BeforeAllCallbacks, if necessary,
// and store the instance in the ExtensionContext.
ClassExtensionContext extensionContext = (ClassExtensionContext) context.getExtensionContext();
throwableCollector.execute(() -> extensionContext.setTestInstance(
context.getTestInstanceProvider().getTestInstance(Optional.empty())));
throwableCollector.execute(() -> extensionContext.setTestInstances(
context.getTestInstancesProvider().getTestInstances(Optional.empty())));
}

if (throwableCollector.isEmpty()) {
Expand Down Expand Up @@ -251,40 +253,43 @@ private TestInstanceFactory resolveTestInstanceFactory(ExtensionRegistry registr
return null;
}

private TestInstanceProvider testInstanceProvider(JupiterEngineExecutionContext parentExecutionContext,
private TestInstancesProvider testInstancesProvider(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, ClassExtensionContext extensionContext) {

TestInstanceProvider testInstanceProvider = childRegistry -> instantiateAndPostProcessTestInstance(
TestInstancesProvider testInstancesProvider = childRegistry -> instantiateAndPostProcessTestInstance(
parentExecutionContext, extensionContext, childRegistry.orElse(registry));

return childRegistry -> extensionContext.getTestInstance().orElseGet(
() -> testInstanceProvider.getTestInstance(childRegistry));
return childRegistry -> extensionContext.getTestInstances().orElseGet(
() -> testInstancesProvider.getTestInstances(childRegistry));
}

private Object instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
ExtensionContext extensionContext, ExtensionRegistry registry) {

Object instance = instantiateTestClass(parentExecutionContext, registry, extensionContext);
invokeTestInstancePostProcessors(instance, registry, extensionContext);
TestInstances instances = instantiateTestClass(parentExecutionContext, registry, extensionContext);
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, extensionContext);
// In addition, we register extensions from instance fields here since the
// best time to do that is immediately following test class instantiation
// and post processing.
registerExtensionsFromFields(registry, this.testClass, instance);
return instance;
registerExtensionsFromFields(registry, this.testClass, instances.getInnermostInstance());
return instances;
}

protected Object instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, ExtensionContext extensionContext) {

return instantiateTestClass(Optional.empty(), registry, extensionContext);
}

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

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

private Object invokeTestInstanceFactory(Optional<Object> outerInstance, ExtensionContext extensionContext) {
Expand Down Expand Up @@ -427,11 +432,11 @@ private AfterEachMethodAdapter synthesizeAfterEachMethodAdapter(Method method) {
}

private void invokeMethodInExtensionContext(Method method, ExtensionContext context, ExtensionRegistry registry) {
Object testInstance = context.getRequiredTestInstance();
testInstance = ReflectionUtils.getOutermostInstance(testInstance, method.getDeclaringClass()).orElseThrow(
TestInstances testInstances = context.getRequiredTestInstances();
Object target = testInstances.findInstance(method.getDeclaringClass()).orElseThrow(
() -> new JUnitException("Failed to find instance for method: " + method.toGenericString()));

executableInvoker.invoke(method, testInstance, context, registry);
executableInvoker.invoke(method, target, context, registry);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.platform.engine.EngineExecutionListener;

Expand Down Expand Up @@ -53,6 +54,11 @@ public Optional<Object> getTestInstance() {
return Optional.empty();
}

@Override
public Optional<TestInstances> getTestInstances() {
return Optional.empty();
}

@Override
public Optional<Method> getTestMethod() {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
Expand All @@ -29,17 +30,17 @@
@API(status = INTERNAL, since = "5.0")
public final class MethodExtensionContext extends AbstractExtensionContext<TestMethodTestDescriptor> {

private final Object testInstance;
private final TestInstances testInstances;

private final ThrowableCollector throwableCollector;

public MethodExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
TestMethodTestDescriptor testDescriptor, JupiterConfiguration configuration, Object testInstance,
TestMethodTestDescriptor testDescriptor, JupiterConfiguration configuration, TestInstances testInstances,
ThrowableCollector throwableCollector) {

super(parent, engineExecutionListener, testDescriptor, configuration);

this.testInstance = testInstance;
this.testInstances = testInstances;
this.throwableCollector = throwableCollector;
}

Expand All @@ -60,7 +61,12 @@ public Optional<Lifecycle> getTestInstanceLifecycle() {

@Override
public Optional<Object> getTestInstance() {
return Optional.of(this.testInstance);
return Optional.of(this.testInstances.getInnermostInstance());
}

@Override
public Optional<TestInstances> getTestInstances() {
return Optional.of(this.testInstances);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

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;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
Expand Down Expand Up @@ -63,14 +64,14 @@ public final Set<TestTag> getTags() {
// --- Node ----------------------------------------------------------------

@Override
protected Object instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, ExtensionContext extensionContext) {

// Extensions registered for nested classes and below are not to be used for instantiating outer classes
Optional<ExtensionRegistry> childExtensionRegistryForOuterInstance = Optional.empty();
Object outerInstance = parentExecutionContext.getTestInstanceProvider().getTestInstance(
TestInstances outerInstances = parentExecutionContext.getTestInstancesProvider().getTestInstances(
childExtensionRegistryForOuterInstance);
return instantiateTestClass(Optional.of(outerInstance), registry, extensionContext);
return instantiateTestClass(Optional.of(outerInstances), registry, extensionContext);
}

}
Loading

0 comments on commit f42aede

Please sign in to comment.