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 10, 2017
1 parent 3008b90 commit d54d4df
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD;
import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME;
import static org.junit.jupiter.engine.descriptor.TestInstanceLifecycleUtils.getDefaultTestInstanceLifecycle;
import static org.junit.jupiter.engine.descriptor.TestInstanceLifecycleUtils.getTestInstanceLifecycle;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.platform.commons.util.PreconditionViolationException;
import org.junit.platform.engine.ConfigurationParameters;

/**
* Unit tests for {@link TestInstanceLifecycleUtils}.
*
* <p>NOTE: it doesn't make sense to unit test the JVM system property fallback
* support in this test class since that feature is a concrete implementation
* detail of {@code LauncherConfigurationParameters} which necessitates an
* integration test via the {@code Launcher} API.
*
* @since 5.0
*/
class TestInstanceLifecycleUtilsTests {

private static final String KEY = DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME;

@Test
void getDefaultTestInstanceLifecyclePreconditions() {
PreconditionViolationException exception = assertThrows(PreconditionViolationException.class,
() -> getDefaultTestInstanceLifecycle(null));
assertThat(exception).hasMessage("ConfigurationParameters must not be null");
}

@Test
void getDefaultTestInstanceLifecycleWithNoConfigParamSet() {
Lifecycle lifecycle = getDefaultTestInstanceLifecycle(mock(ConfigurationParameters.class));
assertThat(lifecycle).isEqualTo(PER_METHOD);
}

@Test
void getDefaultTestInstanceLifecycleWithConfigParamSet() {
assertAll(//
() -> assertDefaultConfigParam(null, PER_METHOD), //
() -> assertDefaultConfigParam("", PER_METHOD), //
() -> assertDefaultConfigParam("bogus", PER_METHOD), //
() -> assertDefaultConfigParam(PER_METHOD.name(), PER_METHOD), //
() -> assertDefaultConfigParam(PER_METHOD.name().toLowerCase(), PER_METHOD), //
() -> assertDefaultConfigParam(" " + PER_METHOD.name() + " ", PER_METHOD), //
() -> assertDefaultConfigParam(PER_CLASS.name(), PER_CLASS), //
() -> assertDefaultConfigParam(PER_CLASS.name().toLowerCase(), PER_CLASS), //
() -> assertDefaultConfigParam(" " + PER_CLASS.name() + " ", Lifecycle.PER_CLASS) //
);
}

private void assertDefaultConfigParam(String configValue, Lifecycle expected) {
ConfigurationParameters configParams = mock(ConfigurationParameters.class);
when(configParams.get(KEY)).thenReturn(Optional.ofNullable(configValue));
Lifecycle lifecycle = getDefaultTestInstanceLifecycle(configParams);
assertThat(lifecycle).isEqualTo(expected);
}

@Test
void getTestInstanceLifecyclePreconditions() {
PreconditionViolationException exception = assertThrows(PreconditionViolationException.class,
() -> getTestInstanceLifecycle(null, mock(ConfigurationParameters.class)));
assertThat(exception).hasMessage("testClass must not be null");

exception = assertThrows(PreconditionViolationException.class,
() -> getTestInstanceLifecycle(getClass(), null));
assertThat(exception).hasMessage("ConfigurationParameters must not be null");
}

@Test
void getTestInstanceLifecycleWithNoConfigParamSet() {
Lifecycle lifecycle = getTestInstanceLifecycle(getClass(), mock(ConfigurationParameters.class));
assertThat(lifecycle).isEqualTo(PER_METHOD);
}

@Test
void getTestInstanceLifecycleWithConfigParamSet() {
ConfigurationParameters configParams = mock(ConfigurationParameters.class);
when(configParams.get(KEY)).thenReturn(Optional.of(PER_CLASS.name().toLowerCase()));
Lifecycle lifecycle = getTestInstanceLifecycle(getClass(), configParams);
assertThat(lifecycle).isEqualTo(PER_CLASS);
}

@Test
void getTestInstanceLifecycleWithLocalConfigThatOverridesCustomDefaultSetViaConfigParam() {
ConfigurationParameters configParams = mock(ConfigurationParameters.class);
when(configParams.get(KEY)).thenReturn(Optional.of(PER_CLASS.name().toLowerCase()));
Lifecycle lifecycle = getTestInstanceLifecycle(TestCase.class, configParams);
assertThat(lifecycle).isEqualTo(PER_METHOD);
}

@TestInstance(Lifecycle.PER_METHOD)
private static class TestCase {
}

}

0 comments on commit d54d4df

Please sign in to comment.