Skip to content

Commit

Permalink
Introduce config param for default test instance lifecycle
Browse files Browse the repository at this point in the history
WIP

Issue: #905
  • Loading branch information
sbrannen committed Aug 12, 2017
1 parent 7b2d29f commit 0718b23
Show file tree
Hide file tree
Showing 8 changed files with 487 additions and 19 deletions.
17 changes: 17 additions & 0 deletions documentation/src/docs/asciidoc/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,23 @@ test instance lifecycle mode.
NOTE: In the context of test instance lifecycle a _test_ method is any method annotated
with `@Test`, `@RepeatedTest`, `@ParameterizedTest`, `@TestFactory`, or `@TestTemplate`.

[[writing-tests-test-instance-lifecycle-changing-default]]
==== Changing the Default Test Instance Lifecycle

If a test class or test interface is not annotated with `@TestInstance`, JUnit Jupiter
will use a _default_ lifecycle mode. The standard _default_ mode is `PER_METHOD`;
however, it is possible to change the _default_ for the execution of an entire test plan.
To change the default test instance lifecycle mode, simply set the
`junit.jupiter.testinstance.lifecycle.default` configuration parameter to the name of an
enum constant defined in `TestInstance.Lifecycle`, ignoring case. This can be supplied as
a JVM system property or as a _configuration parameter_ in the `LauncherDiscoveryRequest`
that is passed to the `Launcher`.

For example, to set the default test instance lifecycle mode to `Lifecycle.PER_CLASS`,
you can start your JVM with the following system property.

`-Djunit.jupiter.testinstance.lifecycle.default=per_class`

[[writing-tests-nested]]
=== Nested Tests

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
* <p>If {@code @TestInstance} is not declared on a test class or implemented
* test interface, the lifecycle mode will implicitly default to
* {@link Lifecycle#PER_METHOD PER_METHOD}. Note, however, that an explicit
* lifecycle mode is <em>inherited</em> within a test class hierarchy.
* lifecycle mode is <em>inherited</em> within a test class hierarchy. In
* addition, the <em>default</em> lifecycle mode may be set via the
* {@code junit.jupiter.testinstance.lifecycle.default} <em>configuration
* parameter</em> which can be supplied via the {@code Launcher} API or via a
* JVM system property. Consult the User Guide for further information.
*
* <h3>Use Cases</h3>
* <p>Setting the test instance lifecycle mode to {@link Lifecycle#PER_CLASS
Expand Down Expand Up @@ -57,6 +61,9 @@

/**
* Enumeration of test instance lifecycle <em>modes</em>.
*
* @see #PER_METHOD
* @see #PER_CLASS
*/
enum Lifecycle {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ public final class Constants {
*/
public static final String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled";

/**
* Property name used to set the default test instance lifecycle mode: {@value}
*
* <h3>Supported Values</h3>
*
* <p>Supported values include names of enum constants defined in
* {@link org.junit.jupiter.api.TestInstance.Lifecycle}, ignoring case.
*
* <p>If not specified, the default is "per_method" which corresponds to
* {@code @TestInstance(Lifecycle.PER_METHOD)}.
*
* @see org.junit.jupiter.api.TestInstance
*/
public static final String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = "junit.jupiter.testinstance.lifecycle.default";

private Constants() {
/* no-op */
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterEachMethods;
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findBeforeAllMethods;
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findBeforeEachMethods;
import static org.junit.jupiter.engine.descriptor.TestInstanceLifecycleUtils.getTestInstanceLifecycle;
import static org.junit.platform.commons.meta.API.Usage.Internal;

import java.lang.reflect.Constructor;
Expand All @@ -24,7 +25,6 @@
import java.util.Set;
import java.util.function.Function;

import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
Expand All @@ -40,7 +40,6 @@
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.AnnotationUtils;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ReflectionUtils;
import org.junit.platform.engine.TestDescriptor;
Expand All @@ -65,7 +64,6 @@ public class ClassTestDescriptor extends JupiterTestDescriptor {
private static final ExecutableInvoker executableInvoker = new ExecutableInvoker();

private final Class<?> testClass;
private final Lifecycle lifecycle;

private List<Method> beforeAllMethods;
private List<Method> afterAllMethods;
Expand All @@ -83,7 +81,6 @@ protected ClassTestDescriptor(UniqueId uniqueId, Function<Class<?>, String> defa
defaultDisplayNameGenerator), new ClassSource(testClass));

this.testClass = testClass;
this.lifecycle = getTestInstanceLifecycle(testClass);
}

// --- TestDescriptor ------------------------------------------------------
Expand Down Expand Up @@ -117,8 +114,10 @@ private static String generateDefaultDisplayName(Class<?> testClass) {

@Override
public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) {
this.beforeAllMethods = findBeforeAllMethods(testClass, this.lifecycle == Lifecycle.PER_METHOD);
this.afterAllMethods = findAfterAllMethods(testClass, this.lifecycle == Lifecycle.PER_METHOD);
Lifecycle lifecycle = getTestInstanceLifecycle(testClass, context.getConfigurationParameters());

this.beforeAllMethods = findBeforeAllMethods(testClass, lifecycle == Lifecycle.PER_METHOD);
this.afterAllMethods = findAfterAllMethods(testClass, lifecycle == Lifecycle.PER_METHOD);
this.beforeEachMethods = findBeforeEachMethods(testClass);
this.afterEachMethods = findAfterEachMethods(testClass);

Expand All @@ -134,7 +133,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte

// @formatter:off
return context.extend()
.withTestInstanceProvider(testInstanceProvider(context, registry, extensionContext))
.withTestInstanceProvider(testInstanceProvider(context, registry, extensionContext, lifecycle))
.withExtensionRegistry(registry)
.withExtensionContext(extensionContext)
.withThrowableCollector(throwableCollector)
Expand Down Expand Up @@ -168,9 +167,9 @@ public void after(JupiterEngineExecutionContext context) throws Exception {
}

private TestInstanceProvider testInstanceProvider(JupiterEngineExecutionContext parentExecutionContext,
ExtensionRegistry registry, ClassExtensionContext extensionContext) {
ExtensionRegistry registry, ClassExtensionContext extensionContext, Lifecycle lifecycle) {

if (this.lifecycle == Lifecycle.PER_CLASS) {
if (lifecycle == Lifecycle.PER_CLASS) {
// Eagerly load test instance for BeforeAllCallbacks, if necessary,
// and store the instance in the ExtensionContext.
Object instance = instantiateAndPostProcessTestInstance(parentExecutionContext, extensionContext, registry);
Expand Down Expand Up @@ -283,20 +282,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(
() -> new JUnitException("Failed to find instance for method: " + method.toGenericString()));

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

private static TestInstance.Lifecycle getTestInstanceLifecycle(Class<?> testClass) {
// @formatter:off
return AnnotationUtils.findAnnotation(testClass, TestInstance.class)
.map(TestInstance::value)
.orElse(Lifecycle.PER_METHOD);
// @formatter:on
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2015-2017 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 v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.engine.descriptor;

import static java.util.logging.Level.WARNING;
import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME;

import java.util.Optional;
import java.util.logging.Logger;

import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.platform.commons.util.AnnotationUtils;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.ConfigurationParameters;

/**
* Collection of utilities for retrieving the test instance lifecycle mode.
*
* @since 5.0
* @see TestInstance
* @see TestInstance.Lifecycle
*/
final class TestInstanceLifecycleUtils {

private static final Logger LOG = Logger.getLogger(TestInstanceLifecycleUtils.class.getName());

///CLOVER:OFF
private TestInstanceLifecycleUtils() {
/* no-op */
}
///CLOVER:ON

static TestInstance.Lifecycle getTestInstanceLifecycle(Class<?> testClass, ConfigurationParameters configParams) {
Preconditions.notNull(testClass, "testClass must not be null");
Preconditions.notNull(configParams, "ConfigurationParameters must not be null");

// @formatter:off
return AnnotationUtils.findAnnotation(testClass, TestInstance.class)
.map(TestInstance::value)
.orElseGet(() -> getDefaultTestInstanceLifecycle(configParams));
// @formatter:on
}

// TODO Consider looking up the default test instance lifecycle mode once per test plan execution.
static TestInstance.Lifecycle getDefaultTestInstanceLifecycle(ConfigurationParameters configParams) {
Preconditions.notNull(configParams, "ConfigurationParameters must not be null");
String propertyName = DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME;

Optional<String> optional = configParams.get(propertyName);
String constantName = null;
if (optional.isPresent()) {
try {
constantName = optional.get().trim().toUpperCase();
Lifecycle lifecycle = TestInstance.Lifecycle.valueOf(constantName);
LOG.info(() -> String.format(
"Using default test instance lifecycle mode '%s' set via the '%s' configuration parameter.",
lifecycle, propertyName));
return lifecycle;
}
catch (Exception ex) {
// local copy necessary for use in lambda expression
String constant = constantName;
LOG.log(WARNING, ex,
() -> String.format(
"Invalid test instance lifecycle mode '%s' set via the '%s' configuration parameter. "
+ "Falling back to %s lifecycle semantics.",
constant, propertyName, Lifecycle.PER_METHOD.name()));
}
}

return Lifecycle.PER_METHOD;
}

}
Loading

0 comments on commit 0718b23

Please sign in to comment.