Skip to content

Commit

Permalink
Introduce config param for default test instance lifecycle
Browse files Browse the repository at this point in the history
Prior to this commit, the test instance lifecycle mode could only be
changed from the default per-method value to per-class by annotating
every single test class or test interface with @testinstance(PER_CLASS).

This commit addresses this issue by introducing a new configuration
parameter that allows the default test instance lifecycle semantics to
be set on a per-project basis (e.g., for a build).

Specifically, the default test instance lifecycle mode can now be set
via a configuration parameter or JVM system property named
`junit.jupiter.testinstance.lifecycle.default` with a value equal to
one of the enum constants in TestInstance.Lifecycle.

Issue: #905
  • Loading branch information
sbrannen committed Aug 12, 2017
1 parent 7b2d29f commit f750c85
Show file tree
Hide file tree
Showing 9 changed files with 490 additions and 20 deletions.
4 changes: 3 additions & 1 deletion documentation/src/docs/asciidoc/release-notes-5.0.0-RC3.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ on GitHub.

===== New Features and Improvements

* ❓
* The _default_ test instance lifecycle mode can now be set via a _configuration
parameter_ or JVM system property named `junit.jupiter.testinstance.lifecycle.default`.
See <<writing-tests-test-instance-lifecycle-changing-default>> for details.


[[release-notes-5.0.0-rc3-junit-vintage]]
Expand Down
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 f750c85

Please sign in to comment.