From 5e73dfc13cbbe414cf51acd3c049c0b091e11ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=B6f?= Date: Wed, 13 Nov 2024 19:56:39 +0100 Subject: [PATCH] add support for JUnit 5 TestInstance.Lifecycle.PER_CLASS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jonas Höf --- .../valueprovider/ValueProviderExtension.java | 38 +++-- .../tngtech/valueprovider/JUnit5Tests.java | 12 ++ ...clePerClassAllDisabledTestMethodsTest.java | 37 +++++ ...LifecyclePerClassDataProviderDemoTest.java | 132 ++++++++++++++++++ ...yclePerClassParameterizedTestDemoTest.java | 128 +++++++++++++++++ 5 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassAllDisabledTestMethodsTest.java create mode 100644 junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassDataProviderDemoTest.java create mode 100644 junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassParameterizedTestDemoTest.java diff --git a/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java b/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java index ff1767c..9265c26 100644 --- a/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java +++ b/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java @@ -4,21 +4,15 @@ import java.lang.reflect.Method; import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.InvocationInterceptor; -import org.junit.jupiter.api.extension.ReflectiveInvocationContext; -import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.tngtech.valueprovider.ValueProviderExtension.TestMethodCycleState.BEFORE_FIRST_CYCLE; -import static com.tngtech.valueprovider.ValueProviderExtension.TestMethodCycleState.CYCLE_COMLETED; -import static com.tngtech.valueprovider.ValueProviderExtension.TestMethodCycleState.CYCLE_STARTED; +import static com.tngtech.valueprovider.ValueProviderExtension.TestMethodCycleState.*; import static java.lang.System.identityHashCode; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; public class ValueProviderExtension implements BeforeAllCallback, AfterAllCallback, @@ -47,7 +41,7 @@ enum TestMethodCycleState { public void beforeAll(ExtensionContext context) { logger.debug("{} beforeAll {}", identityHashCode(this), getTestClassName(context)); - startTestClassCycle(); + startTestClassCycleIf(context, PER_METHOD); } @Override @@ -55,6 +49,7 @@ public T interceptTestClassConstructor(Invocation invocation, ReflectiveI ExtensionContext extensionContext) throws Throwable { logger.debug("{} interceptTestClassConstructor {}", identityHashCode(this), buildQualifiedTestMethodName(extensionContext)); + startTestClassCycleIf(extensionContext, PER_CLASS); ensureStaticInitializationOfTestClass(extensionContext); startTestMethodCycle(); return invocation.proceed(); @@ -89,16 +84,23 @@ public void handleTestExecutionException(ExtensionContext context, Throwable thr public void afterEach(ExtensionContext context) { logger.debug("{} afterEach {}", identityHashCode(this), buildQualifiedTestMethodName(context)); - finishTestMethodCycle(); + finishTestMethodCycleIf(context, PER_METHOD); } @Override public void afterAll(ExtensionContext context) { logger.debug("{} afterAll {}", identityHashCode(this), getTestClassName(context)); + finishTestMethodCycleIf(context, PER_CLASS); finishTestClassCycle(); } + private void startTestClassCycleIf(ExtensionContext context, Lifecycle lifecycle) { + if (isLifecycle(context, lifecycle)) { + startTestClassCycle(); + } + } + private void startTestClassCycle() { ValueProviderFactory.startTestClassCycle(); resetTestMethodCycleState(); @@ -123,6 +125,12 @@ private void finishTestMethodCycleIfNecessary() { } } + private void finishTestMethodCycleIf(ExtensionContext context, Lifecycle lifecycle) { + if (isLifecycle(context, lifecycle)) { + finishTestMethodCycle(); + } + } + private void finishTestMethodCycle() { ValueProviderFactory.finishTestMethodCycle(); testMethodCycleState = CYCLE_COMLETED; @@ -150,4 +158,8 @@ private static String getTestMethodName(ExtensionContext context) { .map(Method::getName) .orElse(""); } + + private static boolean isLifecycle(ExtensionContext context, Lifecycle lifecycle) { + return lifecycle == context.getTestInstanceLifecycle().orElse(null); + } } diff --git a/junit5/src/test/java/com/tngtech/valueprovider/JUnit5Tests.java b/junit5/src/test/java/com/tngtech/valueprovider/JUnit5Tests.java index b33f1d2..f63baf7 100644 --- a/junit5/src/test/java/com/tngtech/valueprovider/JUnit5Tests.java +++ b/junit5/src/test/java/com/tngtech/valueprovider/JUnit5Tests.java @@ -1,7 +1,12 @@ package com.tngtech.valueprovider; +import java.util.List; +import java.util.Objects; + import static com.tngtech.valueprovider.ValueProviderAsserter.reinitializeTestClassSeed; import static com.tngtech.valueprovider.ValueProviderAsserter.setSeedProperties; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; public final class JUnit5Tests { /** @@ -16,4 +21,11 @@ public static void ensureDefinedFactoryState() { setSeedProperties(); reinitializeTestClassSeed(); } + + public static List asListWithoutNulls(ValueProvider... valueProviders) { + return stream(valueProviders) + .filter(Objects::nonNull) + .collect(toList()); + } + } diff --git a/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassAllDisabledTestMethodsTest.java b/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassAllDisabledTestMethodsTest.java new file mode 100644 index 0000000..ecd97c4 --- /dev/null +++ b/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassAllDisabledTestMethodsTest.java @@ -0,0 +1,37 @@ +package com.tngtech.valueprovider; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +/** + * No need for different sequences of enabled/disabled test methods + * like for {@link Lifecycle#PER_CLASS}, + * as test method cycle is started once in intercepted constructor, + * and only finished after last test method. + * + * @see DisabledEnabledDisabledTestMethodsTest + * @see EnabledDisabledEnabledTestMethodsTest + */ +@TestInstance(PER_CLASS) +@ExtendWith(ValueProviderExtension.class) +class LifecyclePerClassAllDisabledTestMethodsTest { + @Disabled + @Test + void a_test_disabled() { + } + + @Disabled + @Test + void b_test_disabled() { + } + + @Disabled + @Test + void c_test_disabled() { + } +} diff --git a/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassDataProviderDemoTest.java b/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassDataProviderDemoTest.java new file mode 100644 index 0000000..98ce7a4 --- /dev/null +++ b/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassDataProviderDemoTest.java @@ -0,0 +1,132 @@ +package com.tngtech.valueprovider; + +import java.util.ArrayList; +import java.util.List; + +import com.tngtech.junit.dataprovider.DataProvider; +import com.tngtech.junit.dataprovider.UseDataProvider; +import com.tngtech.junit.dataprovider.UseDataProviderExtension; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer.MethodName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.tngtech.junit.dataprovider.DataProviders.$; +import static com.tngtech.junit.dataprovider.DataProviders.$$; +import static com.tngtech.valueprovider.JUnit5Tests.asListWithoutNulls; +import static com.tngtech.valueprovider.JUnit5Tests.ensureDefinedFactoryState; +import static com.tngtech.valueprovider.ValueProviderFactory.createRandomValueProvider; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@TestInstance(PER_CLASS) +@TestMethodOrder(MethodName.class) +@ExtendWith({ValueProviderExtension.class, UseDataProviderExtension.class}) +class LifecyclePerClassDataProviderDemoTest { + private static final Logger logger = LoggerFactory.getLogger(LifecyclePerClassDataProviderDemoTest.class); + private static final ValueProvider classRandom1; + private static final ValueProvider classRandom2; + + static { + logger.debug("{}: static initialization", LifecyclePerClassDataProviderDemoTest.class.getSimpleName()); + ensureDefinedFactoryState(); + classRandom1 = createRandomValueProvider(); + classRandom2 = createRandomValueProvider(); + } + + private final ValueProvider instanceRandom = createRandomValueProvider(); + private ValueProvider beforeAllRandom; + private ValueProvider dataProviderRandom; + private ValueProvider beforeEachRandom; + private ValueProvider methodRandom; + + private final List randomsOfPreviousTestMethods = new ArrayList<>(); + + @BeforeAll + void beforeAll() { + beforeAllRandom = createRandomValueProvider(); + } + + @BeforeEach + void beforeEach() { + beforeEachRandom = createRandomValueProvider(); + } + + @AfterEach + void resetTestMethodRandoms() { + dataProviderRandom = null; + beforeEachRandom = null; + methodRandom = null; + } + + @DataProvider + Object[][] testValues1() { + logger.debug("create DataProvider 1"); + dataProviderRandom = createRandomValueProvider(); + return $$( + $(dataProviderRandom.fixedDecoratedString("1")), + $(dataProviderRandom.fixedDecoratedString("2")) + ); + } + + @TestTemplate + @UseDataProvider("testValues1") + void a_should_ensure_reproducible_ValueProvider_creation_for_DataProvider(String testValue) { + assertThat(testValue).isNotEmpty(); + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(dataProviderRandom, beforeEachRandom, methodRandom); + } + + @TestTemplate + @UseDataProvider("testValues1") + void b_should_ensure_reproducible_ValueProvider_creation_for_same_DataProvider(String testValue) { + assertThat(testValue).isNotEmpty(); + methodRandom = createRandomValueProvider(); + // @DataProvider is invoked ONCE BEFORE FIRST test method using it + verifyReproducibleValueProviderCreation(beforeEachRandom, methodRandom); + } + + @DataProvider + Object[][] testValues2() { + logger.debug("create DataProvider 2"); + dataProviderRandom = createRandomValueProvider(); + return $$( + $(dataProviderRandom.fixedDecoratedString("1")), + $(dataProviderRandom.fixedDecoratedString("2")) + ); + } + + @TestTemplate + @UseDataProvider("testValues2") + void c_should_ensure_proper_separation_of_test_class_and_test_method_cycles_for_DataProvider(String testValue) { + assertThat(testValue).isNotEmpty(); + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(dataProviderRandom, beforeEachRandom, methodRandom); + } + + @Test + void d_should_ensure_reproducible_ValueProvider_creation() { + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(beforeEachRandom, methodRandom); + } + + @Test + void e_should_ensure_proper_separation_of_test_class_and_test_method_cycles() { + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(beforeEachRandom, methodRandom); + } + + private void verifyReproducibleValueProviderCreation(ValueProvider... additionalMethodRandoms) { + List additionalMethodRandomList = asListWithoutNulls(additionalMethodRandoms); + new ValueProviderAsserter() + .addExpectedTestClassRandomValues(classRandom1, classRandom2) + .addExpectedTestMethodRandomValues(instanceRandom, beforeAllRandom) + .addExpectedTestMethodRandomValues(randomsOfPreviousTestMethods) + .addExpectedTestMethodRandomValues(additionalMethodRandomList) + .assertAllTestClassRandomValues() + .assertAllTestMethodRandomValues() + .assertAllSuffixes(); + randomsOfPreviousTestMethods.addAll(additionalMethodRandomList); + } +} diff --git a/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassParameterizedTestDemoTest.java b/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassParameterizedTestDemoTest.java new file mode 100644 index 0000000..ae487ef --- /dev/null +++ b/junit5/src/test/java/com/tngtech/valueprovider/LifecyclePerClassParameterizedTestDemoTest.java @@ -0,0 +1,128 @@ +package com.tngtech.valueprovider; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer.MethodName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.tngtech.valueprovider.JUnit5Tests.asListWithoutNulls; +import static com.tngtech.valueprovider.JUnit5Tests.ensureDefinedFactoryState; +import static com.tngtech.valueprovider.ValueProviderFactory.createRandomValueProvider; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@TestInstance(PER_CLASS) +@TestMethodOrder(MethodName.class) +@ExtendWith({ValueProviderExtension.class}) +class LifecyclePerClassParameterizedTestDemoTest { + private static final Logger logger = LoggerFactory.getLogger(LifecyclePerClassParameterizedTestDemoTest.class); + private static final ValueProvider classRandom1; + private static final ValueProvider classRandom2; + + static { + logger.debug("{}: static initialization", LifecyclePerClassParameterizedTestDemoTest.class.getSimpleName()); + ensureDefinedFactoryState(); + classRandom1 = createRandomValueProvider(); + classRandom2 = createRandomValueProvider(); + } + + private final ValueProvider instanceRandom = createRandomValueProvider(); + private ValueProvider beforeAllRandom; + private ValueProvider methodSourceRandom; + private ValueProvider beforeEachRandom; + private ValueProvider methodRandom; + + private final List randomsOfPreviousTestMethods = new ArrayList<>(); + + @BeforeAll + void beforeAll() { + beforeAllRandom = createRandomValueProvider(); + } + + @BeforeEach + void beforeEach() { + beforeEachRandom = createRandomValueProvider(); + } + + @AfterEach + void resetTestMethodRandoms() { + methodSourceRandom = null; + beforeEachRandom = null; + methodRandom = null; + } + + private Stream testValues1() { + logger.debug("create MethodSource 1"); + methodSourceRandom = createRandomValueProvider(); + return Stream.of( + methodSourceRandom.fixedDecoratedString("1"), + methodSourceRandom.fixedDecoratedString("2") + ); + } + + @ParameterizedTest + @MethodSource("testValues1") + void a_should_ensure_reproducible_ValueProvider_creation_for_MethodSource(String testValue) { + assertThat(testValue).isNotEmpty(); + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(methodSourceRandom, beforeEachRandom, methodRandom); + } + + @ParameterizedTest + @MethodSource("testValues1") + void b_should_ensure_reproducible_ValueProvider_creation_for_same_MethodSource(String testValue) { + assertThat(testValue).isNotEmpty(); + methodRandom = createRandomValueProvider(); + // @MethodSource is invoked for BEFORE EVERY test method using it + verifyReproducibleValueProviderCreation(methodSourceRandom, beforeEachRandom, methodRandom); + } + + private Stream testValues2() { + logger.debug("create MethodSource 2"); + methodSourceRandom = createRandomValueProvider(); + return Stream.of( + methodSourceRandom.fixedDecoratedString("1"), + methodSourceRandom.fixedDecoratedString("2") + ); + } + + @ParameterizedTest + @MethodSource("testValues2") + void c_should_ensure_proper_separation_of_test_class_and_test_method_cycles_for_MethodSource(String testValue) { + assertThat(testValue).isNotEmpty(); + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(methodSourceRandom, beforeEachRandom, methodRandom); + } + + @Test + void d_should_ensure_reproducible_ValueProvider_creation() { + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(beforeEachRandom, methodRandom); + } + + @Test + void e_should_ensure_proper_separation_of_test_class_and_test_method_cycles() { + methodRandom = createRandomValueProvider(); + verifyReproducibleValueProviderCreation(beforeEachRandom, methodRandom); + } + + private void verifyReproducibleValueProviderCreation(ValueProvider... additionalMethodRandoms) { + List additionalMethodRandomList = asListWithoutNulls(additionalMethodRandoms); + new ValueProviderAsserter() + .addExpectedTestClassRandomValues(classRandom1, classRandom2) + .addExpectedTestMethodRandomValues(instanceRandom, beforeAllRandom) + .addExpectedTestMethodRandomValues(randomsOfPreviousTestMethods) + .addExpectedTestMethodRandomValues(additionalMethodRandomList) + .assertAllTestClassRandomValues() + .assertAllTestMethodRandomValues() + .assertAllSuffixes(); + randomsOfPreviousTestMethods.addAll(additionalMethodRandomList); + } +}