From 34d813d99cf77ddd22d086b1abc1324747389fb5 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 8 Aug 2017 00:45:08 +0300 Subject: [PATCH] Introduce config param for default test instance lifecycle WIP Issue: #905 --- .../asciidoc/release-notes-5.0.0-RC3.adoc | 4 +- .../src/docs/asciidoc/writing-tests.adoc | 17 ++ .../org/junit/jupiter/api/TestInstance.java | 9 +- .../org/junit/jupiter/engine/Constants.java | 15 ++ .../descriptor/ClassTestDescriptor.java | 26 +- .../TestInstanceLifecycleUtils.java | 83 ++++++ ...stInstanceLifecycleConfigurationTests.java | 237 ++++++++++++++++++ .../engine/TestInstanceLifecycleTests.java | 1 + .../TestInstanceLifecycleUtilsTests.java | 118 +++++++++ 9 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtils.java create mode 100644 junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java create mode 100644 junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java diff --git a/documentation/src/docs/asciidoc/release-notes-5.0.0-RC3.adoc b/documentation/src/docs/asciidoc/release-notes-5.0.0-RC3.adoc index a074567f69b8..e4d27bb64c18 100644 --- a/documentation/src/docs/asciidoc/release-notes-5.0.0-RC3.adoc +++ b/documentation/src/docs/asciidoc/release-notes-5.0.0-RC3.adoc @@ -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 <> for details. [[release-notes-5.0.0-rc3-junit-vintage]] diff --git a/documentation/src/docs/asciidoc/writing-tests.adoc b/documentation/src/docs/asciidoc/writing-tests.adoc index 9f1dbcc615c7..4bed936699a7 100644 --- a/documentation/src/docs/asciidoc/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/writing-tests.adoc @@ -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 diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java index 37516dd95a18..1ef173f3cede 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java @@ -25,7 +25,11 @@ *

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 inherited within a test class hierarchy. + * lifecycle mode is inherited within a test class hierarchy. In + * addition, the default lifecycle mode may be set via the + * {@code junit.jupiter.testinstance.lifecycle.default} configuration + * parameter which can be supplied via the {@code Launcher} API or via a + * JVM system property. Consult the User Guide for further information. * *

Use Cases

*

Setting the test instance lifecycle mode to {@link Lifecycle#PER_CLASS @@ -57,6 +61,9 @@ /** * Enumeration of test instance lifecycle modes. + * + * @see #PER_METHOD + * @see #PER_CLASS */ enum Lifecycle { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java index 34fc8d446a70..c78ebf4ee382 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java @@ -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} + * + *

Supported Values

+ * + *

Supported values include names of enum constants defined in + * {@link org.junit.jupiter.api.TestInstance.Lifecycle}, ignoring case. + * + *

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 */ } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java index 977aba296d44..e4f3fa9e572d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java @@ -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; @@ -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; @@ -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; @@ -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 beforeAllMethods; private List afterAllMethods; @@ -83,7 +81,6 @@ protected ClassTestDescriptor(UniqueId uniqueId, Function, String> defa defaultDisplayNameGenerator), new ClassSource(testClass)); this.testClass = testClass; - this.lifecycle = getTestInstanceLifecycle(testClass); } // --- TestDescriptor ------------------------------------------------------ @@ -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); @@ -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) @@ -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); @@ -283,7 +282,6 @@ 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())); @@ -291,12 +289,4 @@ private void invokeMethodInExtensionContext(Method method, ExtensionContext cont 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 - } - } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtils.java new file mode 100644 index 000000000000..8e0a23fca789 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtils.java @@ -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 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; + } + +} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java new file mode 100644 index 000000000000..cb71bfea888d --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleConfigurationTests.java @@ -0,0 +1,237 @@ +/* + * 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; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.test.event.ExecutionEventRecorder; +import org.junit.platform.launcher.Launcher; + +/** + * Integration tests for {@link TestInstance @TestInstance} lifecycle + * configuration support, not to be confused with {@link TestInstanceLifecycleTests}. + * + *

Specifically, this class tests custom lifecycle configuration specified + * via {@code @TestInstance} as well as via {@link ConfigurationParameters} + * supplied to the {@link Launcher} or via a JVM system property. + * + * @since 5.0 + * @see TestInstanceLifecycleTests + */ +class TestInstanceLifecycleConfigurationTests extends AbstractJupiterTestEngineTests { + + private static final String KEY = Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME; + + private static final List methodsInvoked = new ArrayList<>(); + + @BeforeEach + @AfterEach + void reset() { + methodsInvoked.clear(); + System.clearProperty(KEY); + } + + @Test + void instancePerMethodUsingStandardDefaultConfiguration() { + performAssertions(AssumedInstancePerTestMethodTestCase.class, 2, 0, 1, "beforeAll", "test", "afterAll"); + } + + @Test + void instancePerClassConfiguredViaExplicitAnnotationDeclaration() { + performAssertions(ExplicitInstancePerClassTestCase.class, 2, 0, 1, "beforeAll", "test", "afterAll"); + } + + @Test + void instancePerClassConfiguredViaSystemProperty() { + Class testClass = AssumedInstancePerClassTestCase.class; + + // Should fail by default... + performAssertions(testClass, 2, 1, 0); + + // Should pass with the system property set + System.setProperty(KEY, PER_CLASS.name()); + performAssertions(testClass, 2, 0, 1, "beforeAll", "test", "afterAll"); + } + + @Test + void instancePerClassConfiguredViaConfigParam() { + Class testClass = AssumedInstancePerClassTestCase.class; + + // Should fail by default... + performAssertions(testClass, 2, 1, 0); + + // Should pass with the config param + performAssertions(testClass, singletonMap(KEY, PER_CLASS.name()), 2, 0, 1, "beforeAll", "test", "afterAll"); + } + + @Test + void instancePerClassConfiguredViaConfigParamThatOverridesSystemProperty() { + Class testClass = AssumedInstancePerClassTestCase.class; + + // Should fail with system property + System.setProperty(KEY, PER_METHOD.name()); + performAssertions(testClass, 2, 1, 0); + + // Should pass with the config param + performAssertions(testClass, singletonMap(KEY, PER_CLASS.name()), 2, 0, 1, "beforeAll", "test", "afterAll"); + } + + @Test + void instancePerMethodConfiguredViaExplicitAnnotationDeclarationThatOverridesSystemProperty() { + System.setProperty(KEY, PER_CLASS.name()); + performAssertions(ExplicitInstancePerTestMethodTestCase.class, 2, 0, 1, "beforeAll", "test", "afterAll"); + } + + @Test + void instancePerMethodConfiguredViaExplicitAnnotationDeclarationThatOverridesConfigParam() { + Class testClass = ExplicitInstancePerTestMethodTestCase.class; + performAssertions(testClass, singletonMap(KEY, PER_CLASS.name()), 2, 0, 1, "beforeAll", "test", "afterAll"); + } + + private void performAssertions(Class testClass, int containers, int containersFailed, int tests, + String... methods) { + + performAssertions(testClass, emptyMap(), containers, containersFailed, tests, methods); + } + + private void performAssertions(Class testClass, Map configParams, int containers, + int failedContainers, int tests, String... methods) { + + // @formatter:off + ExecutionEventRecorder eventRecorder = executeTests( + request() + .selectors(selectClass(testClass)) + .configurationParameters(configParams) + .build() + ); + + assertAll( + () -> assertEquals(containers, eventRecorder.getContainerStartedCount(), "# containers started"), + () -> assertEquals(containers, eventRecorder.getContainerFinishedCount(), "# containers finished"), + () -> assertEquals(failedContainers, eventRecorder.getContainerFailedCount(), "# containers failed"), + () -> assertEquals(tests, eventRecorder.getTestStartedCount(), "# tests started"), + () -> assertEquals(tests, eventRecorder.getTestSuccessfulCount(), "# tests succeeded"), + () -> assertEquals(Arrays.asList(methods), methodsInvoked) + ); + // @formatter:on + } + + // ------------------------------------------------------------------------- + + @TestInstance(PER_METHOD) + private static class ExplicitInstancePerTestMethodTestCase { + + @BeforeAll + static void beforeAll() { + methodsInvoked.add("beforeAll"); + } + + @Test + void test() { + methodsInvoked.add("test"); + } + + @AfterAll + static void afterAllStatic() { + methodsInvoked.add("afterAll"); + } + + } + + /** + * "per-method" lifecycle mode is assumed since the {@code @BeforeAll} and + * {@code @AfterAll} methods are static, even though there is no explicit + * {@code @TestInstance} declaration. + */ + private static class AssumedInstancePerTestMethodTestCase { + + @BeforeAll + static void beforeAll() { + methodsInvoked.add("beforeAll"); + } + + @Test + void test() { + methodsInvoked.add("test"); + } + + @AfterAll + static void afterAllStatic() { + methodsInvoked.add("afterAll"); + } + + } + + @TestInstance(PER_CLASS) + private static class ExplicitInstancePerClassTestCase { + + @BeforeAll + void beforeAll(TestInfo testInfo) { + methodsInvoked.add("beforeAll"); + } + + @Test + void test() { + methodsInvoked.add("test"); + } + + @AfterAll + void afterAll(TestInfo testInfo) { + methodsInvoked.add("afterAll"); + } + + } + + /** + * "per-class" lifecycle mode is assumed since the {@code @BeforeAll} and + * {@code @AfterAll} methods are non-static, even though there is no + * explicit {@code @TestInstance} declaration. + */ + private static class AssumedInstancePerClassTestCase { + + @BeforeAll + void beforeAll(TestInfo testInfo) { + methodsInvoked.add("beforeAll"); + } + + @Test + void test() { + methodsInvoked.add("test"); + } + + @AfterAll + void afterAll(TestInfo testInfo) { + methodsInvoked.add("afterAll"); + } + + } + +} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java index 994adcb598ce..bc430c81b997 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java @@ -63,6 +63,7 @@ * Integration tests for {@link TestInstance @TestInstance} lifecycle support. * * @since 5.0 + * @see TestInstanceLifecycleConfigurationTests */ class TestInstanceLifecycleTests extends AbstractJupiterTestEngineTests { diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java new file mode 100644 index 000000000000..90f6ccdb65bd --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestInstanceLifecycleUtilsTests.java @@ -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}. + * + *

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 { + } + +}