Skip to content

Commit f750c85

Browse files
committed
Introduce config param for default test instance lifecycle
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
1 parent 7b2d29f commit f750c85

File tree

9 files changed

+490
-20
lines changed

9 files changed

+490
-20
lines changed

documentation/src/docs/asciidoc/release-notes-5.0.0-RC3.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ on GitHub.
4848

4949
===== New Features and Improvements
5050

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

5355

5456
[[release-notes-5.0.0-rc3-junit-vintage]]

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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 8 additions & 1 deletion
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

Lines changed: 15 additions & 0 deletions
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

Lines changed: 8 additions & 18 deletions
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
}
Lines changed: 83 additions & 0 deletions
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)