Skip to content

Commit 0718b23

Browse files
committed
Introduce config param for default test instance lifecycle
WIP Issue: #905
1 parent 7b2d29f commit 0718b23

File tree

8 files changed

+487
-19
lines changed

8 files changed

+487
-19
lines changed

documentation/src/docs/asciidoc/writing-tests.adoc

+17
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,23 @@ test instance lifecycle mode.
197197
NOTE: In the context of test instance lifecycle a _test_ method is any method annotated
198198
with `@Test`, `@RepeatedTest`, `@ParameterizedTest`, `@TestFactory`, or `@TestTemplate`.
199199

200+
[[writing-tests-test-instance-lifecycle-changing-default]]
201+
==== Changing the Default Test Instance Lifecycle
202+
203+
If a test class or test interface is not annotated with `@TestInstance`, JUnit Jupiter
204+
will use a _default_ lifecycle mode. The standard _default_ mode is `PER_METHOD`;
205+
however, it is possible to change the _default_ for the execution of an entire test plan.
206+
To change the default test instance lifecycle mode, simply set the
207+
`junit.jupiter.testinstance.lifecycle.default` configuration parameter to the name of an
208+
enum constant defined in `TestInstance.Lifecycle`, ignoring case. This can be supplied as
209+
a JVM system property or as a _configuration parameter_ in the `LauncherDiscoveryRequest`
210+
that is passed to the `Launcher`.
211+
212+
For example, to set the default test instance lifecycle mode to `Lifecycle.PER_CLASS`,
213+
you can start your JVM with the following system property.
214+
215+
`-Djunit.jupiter.testinstance.lifecycle.default=per_class`
216+
200217
[[writing-tests-nested]]
201218
=== Nested Tests
202219

junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java

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

5862
/**
5963
* Enumeration of test instance lifecycle <em>modes</em>.
64+
*
65+
* @see #PER_METHOD
66+
* @see #PER_CLASS
6067
*/
6168
enum Lifecycle {
6269

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java

+15
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ public final class Constants {
7070
*/
7171
public static final String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled";
7272

73+
/**
74+
* Property name used to set the default test instance lifecycle mode: {@value}
75+
*
76+
* <h3>Supported Values</h3>
77+
*
78+
* <p>Supported values include names of enum constants defined in
79+
* {@link org.junit.jupiter.api.TestInstance.Lifecycle}, ignoring case.
80+
*
81+
* <p>If not specified, the default is "per_method" which corresponds to
82+
* {@code @TestInstance(Lifecycle.PER_METHOD)}.
83+
*
84+
* @see org.junit.jupiter.api.TestInstance
85+
*/
86+
public static final String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = "junit.jupiter.testinstance.lifecycle.default";
87+
7388
private Constants() {
7489
/* no-op */
7590
}

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java

+8-18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterEachMethods;
1515
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findBeforeAllMethods;
1616
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findBeforeEachMethods;
17+
import static org.junit.jupiter.engine.descriptor.TestInstanceLifecycleUtils.getTestInstanceLifecycle;
1718
import static org.junit.platform.commons.meta.API.Usage.Internal;
1819

1920
import java.lang.reflect.Constructor;
@@ -24,7 +25,6 @@
2425
import java.util.Set;
2526
import java.util.function.Function;
2627

27-
import org.junit.jupiter.api.TestInstance;
2828
import org.junit.jupiter.api.TestInstance.Lifecycle;
2929
import org.junit.jupiter.api.extension.AfterAllCallback;
3030
import org.junit.jupiter.api.extension.BeforeAllCallback;
@@ -40,7 +40,6 @@
4040
import org.junit.jupiter.engine.extension.ExtensionRegistry;
4141
import org.junit.platform.commons.JUnitException;
4242
import org.junit.platform.commons.meta.API;
43-
import org.junit.platform.commons.util.AnnotationUtils;
4443
import org.junit.platform.commons.util.Preconditions;
4544
import org.junit.platform.commons.util.ReflectionUtils;
4645
import org.junit.platform.engine.TestDescriptor;
@@ -65,7 +64,6 @@ public class ClassTestDescriptor extends JupiterTestDescriptor {
6564
private static final ExecutableInvoker executableInvoker = new ExecutableInvoker();
6665

6766
private final Class<?> testClass;
68-
private final Lifecycle lifecycle;
6967

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

8583
this.testClass = testClass;
86-
this.lifecycle = getTestInstanceLifecycle(testClass);
8784
}
8885

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

118115
@Override
119116
public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) {
120-
this.beforeAllMethods = findBeforeAllMethods(testClass, this.lifecycle == Lifecycle.PER_METHOD);
121-
this.afterAllMethods = findAfterAllMethods(testClass, this.lifecycle == Lifecycle.PER_METHOD);
117+
Lifecycle lifecycle = getTestInstanceLifecycle(testClass, context.getConfigurationParameters());
118+
119+
this.beforeAllMethods = findBeforeAllMethods(testClass, lifecycle == Lifecycle.PER_METHOD);
120+
this.afterAllMethods = findAfterAllMethods(testClass, lifecycle == Lifecycle.PER_METHOD);
122121
this.beforeEachMethods = findBeforeEachMethods(testClass);
123122
this.afterEachMethods = findAfterEachMethods(testClass);
124123

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

135134
// @formatter:off
136135
return context.extend()
137-
.withTestInstanceProvider(testInstanceProvider(context, registry, extensionContext))
136+
.withTestInstanceProvider(testInstanceProvider(context, registry, extensionContext, lifecycle))
138137
.withExtensionRegistry(registry)
139138
.withExtensionContext(extensionContext)
140139
.withThrowableCollector(throwableCollector)
@@ -168,9 +167,9 @@ public void after(JupiterEngineExecutionContext context) throws Exception {
168167
}
169168

170169
private TestInstanceProvider testInstanceProvider(JupiterEngineExecutionContext parentExecutionContext,
171-
ExtensionRegistry registry, ClassExtensionContext extensionContext) {
170+
ExtensionRegistry registry, ClassExtensionContext extensionContext, Lifecycle lifecycle) {
172171

173-
if (this.lifecycle == Lifecycle.PER_CLASS) {
172+
if (lifecycle == Lifecycle.PER_CLASS) {
174173
// Eagerly load test instance for BeforeAllCallbacks, if necessary,
175174
// and store the instance in the ExtensionContext.
176175
Object instance = instantiateAndPostProcessTestInstance(parentExecutionContext, extensionContext, registry);
@@ -283,20 +282,11 @@ private AfterEachMethodAdapter synthesizeAfterEachMethodAdapter(Method method) {
283282
}
284283

285284
private void invokeMethodInExtensionContext(Method method, ExtensionContext context, ExtensionRegistry registry) {
286-
287285
Object testInstance = context.getRequiredTestInstance();
288286
testInstance = ReflectionUtils.getOutermostInstance(testInstance, method.getDeclaringClass()).orElseThrow(
289287
() -> new JUnitException("Failed to find instance for method: " + method.toGenericString()));
290288

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

294-
private static TestInstance.Lifecycle getTestInstanceLifecycle(Class<?> testClass) {
295-
// @formatter:off
296-
return AnnotationUtils.findAnnotation(testClass, TestInstance.class)
297-
.map(TestInstance::value)
298-
.orElse(Lifecycle.PER_METHOD);
299-
// @formatter:on
300-
}
301-
302292
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2015-2017 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v1.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* http://www.eclipse.org/legal/epl-v10.html
9+
*/
10+
11+
package org.junit.jupiter.engine.descriptor;
12+
13+
import static java.util.logging.Level.WARNING;
14+
import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME;
15+
16+
import java.util.Optional;
17+
import java.util.logging.Logger;
18+
19+
import org.junit.jupiter.api.TestInstance;
20+
import org.junit.jupiter.api.TestInstance.Lifecycle;
21+
import org.junit.platform.commons.util.AnnotationUtils;
22+
import org.junit.platform.commons.util.Preconditions;
23+
import org.junit.platform.engine.ConfigurationParameters;
24+
25+
/**
26+
* Collection of utilities for retrieving the test instance lifecycle mode.
27+
*
28+
* @since 5.0
29+
* @see TestInstance
30+
* @see TestInstance.Lifecycle
31+
*/
32+
final class TestInstanceLifecycleUtils {
33+
34+
private static final Logger LOG = Logger.getLogger(TestInstanceLifecycleUtils.class.getName());
35+
36+
///CLOVER:OFF
37+
private TestInstanceLifecycleUtils() {
38+
/* no-op */
39+
}
40+
///CLOVER:ON
41+
42+
static TestInstance.Lifecycle getTestInstanceLifecycle(Class<?> testClass, ConfigurationParameters configParams) {
43+
Preconditions.notNull(testClass, "testClass must not be null");
44+
Preconditions.notNull(configParams, "ConfigurationParameters must not be null");
45+
46+
// @formatter:off
47+
return AnnotationUtils.findAnnotation(testClass, TestInstance.class)
48+
.map(TestInstance::value)
49+
.orElseGet(() -> getDefaultTestInstanceLifecycle(configParams));
50+
// @formatter:on
51+
}
52+
53+
// TODO Consider looking up the default test instance lifecycle mode once per test plan execution.
54+
static TestInstance.Lifecycle getDefaultTestInstanceLifecycle(ConfigurationParameters configParams) {
55+
Preconditions.notNull(configParams, "ConfigurationParameters must not be null");
56+
String propertyName = DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME;
57+
58+
Optional<String> optional = configParams.get(propertyName);
59+
String constantName = null;
60+
if (optional.isPresent()) {
61+
try {
62+
constantName = optional.get().trim().toUpperCase();
63+
Lifecycle lifecycle = TestInstance.Lifecycle.valueOf(constantName);
64+
LOG.info(() -> String.format(
65+
"Using default test instance lifecycle mode '%s' set via the '%s' configuration parameter.",
66+
lifecycle, propertyName));
67+
return lifecycle;
68+
}
69+
catch (Exception ex) {
70+
// local copy necessary for use in lambda expression
71+
String constant = constantName;
72+
LOG.log(WARNING, ex,
73+
() -> String.format(
74+
"Invalid test instance lifecycle mode '%s' set via the '%s' configuration parameter. "
75+
+ "Falling back to %s lifecycle semantics.",
76+
constant, propertyName, Lifecycle.PER_METHOD.name()));
77+
}
78+
}
79+
80+
return Lifecycle.PER_METHOD;
81+
}
82+
83+
}

0 commit comments

Comments
 (0)