From 4aa3819fd72ec83216ef3697501b373f4ef306ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=B6f?= Date: Wed, 20 Nov 2024 13:38:24 +0100 Subject: [PATCH] add support for JUnit 5 @Nested test classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test with relevant combinations of Lifecycle PER_METHOD and PER_CLASS Signed-off-by: Jonas Höf --- .../valueprovider/ValueProviderExtension.java | 75 +++++++-- .../LifecycleMainPerClassNestedDemoTest.java | 127 +++++++++++++++ .../LifecycleMainPerMethodNestedDemoTest.java | 150 ++++++++++++++++++ 3 files changed, 338 insertions(+), 14 deletions(-) create mode 100644 junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerClassNestedDemoTest.java create mode 100644 junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerMethodNestedDemoTest.java diff --git a/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java b/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java index 9265c26..ce31830 100644 --- a/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java +++ b/junit5/src/main/java/com/tngtech/valueprovider/ValueProviderExtension.java @@ -2,6 +2,9 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -45,13 +48,15 @@ public void beforeAll(ExtensionContext context) { } @Override - public T interceptTestClassConstructor(Invocation invocation, ReflectiveInvocationContext> invocationContext, + public T interceptTestClassConstructor( + Invocation invocation, + ReflectiveInvocationContext> invocationContext, ExtensionContext extensionContext) throws Throwable { logger.debug("{} interceptTestClassConstructor {}", identityHashCode(this), buildQualifiedTestMethodName(extensionContext)); startTestClassCycleIf(extensionContext, PER_CLASS); ensureStaticInitializationOfTestClass(extensionContext); - startTestMethodCycle(); + startTestMethodCycle(extensionContext); return invocation.proceed(); } @@ -84,19 +89,23 @@ public void handleTestExecutionException(ExtensionContext context, Throwable thr public void afterEach(ExtensionContext context) { logger.debug("{} afterEach {}", identityHashCode(this), buildQualifiedTestMethodName(context)); - finishTestMethodCycleIf(context, PER_METHOD); + if (testClassHierarchyHasOnlyLifecyclePerMethod(context)) { + finishTestMethodCycle(); + } } @Override public void afterAll(ExtensionContext context) { logger.debug("{} afterAll {}", identityHashCode(this), getTestClassName(context)); - finishTestMethodCycleIf(context, PER_CLASS); - finishTestClassCycle(); + if (isLastTestClassInHierarchyWithLifecyclePerClass(context)) { + finishTestMethodCycle(); + } + finishTestClassCycle(context); } private void startTestClassCycleIf(ExtensionContext context, Lifecycle lifecycle) { - if (isLifecycle(context, lifecycle)) { + if (isLifecycle(context, lifecycle) && !isNestedTestClass(context)) { startTestClassCycle(); } } @@ -106,13 +115,19 @@ private void startTestClassCycle() { resetTestMethodCycleState(); } - private void startTestMethodCycle() { + private void startTestMethodCycle(ExtensionContext context) { + if (isNestedTestClass(context)) { + return; + } finishTestMethodCycleIfNecessary(); ValueProviderFactory.startTestMethodCycle(); testMethodCycleState = CYCLE_STARTED; } - private void finishTestClassCycle() { + private void finishTestClassCycle(ExtensionContext context) { + if (isNestedTestClass(context)) { + return; + } finishTestMethodCycleIfNecessary(); ValueProviderFactory.finishTestClassCycle(); resetTestMethodCycleState(); @@ -125,12 +140,6 @@ private void finishTestMethodCycleIfNecessary() { } } - private void finishTestMethodCycleIf(ExtensionContext context, Lifecycle lifecycle) { - if (isLifecycle(context, lifecycle)) { - finishTestMethodCycle(); - } - } - private void finishTestMethodCycle() { ValueProviderFactory.finishTestMethodCycle(); testMethodCycleState = CYCLE_COMLETED; @@ -162,4 +171,42 @@ private static String getTestMethodName(ExtensionContext context) { private static boolean isLifecycle(ExtensionContext context, Lifecycle lifecycle) { return lifecycle == context.getTestInstanceLifecycle().orElse(null); } + + private static boolean isLastTestClassInHierarchyWithLifecyclePerClass(ExtensionContext context) { + if (!isLifecycle(context, PER_CLASS)) { + return false; + } + Set remainingLifecyclesInHierarchy = determineLifecyclesInTestClassHierarchy(context.getParent()); + return remainingLifecyclesInHierarchy.isEmpty() || containsOnlyLifecyclePerMethod(remainingLifecyclesInHierarchy); + } + + private static boolean testClassHierarchyHasOnlyLifecyclePerMethod(ExtensionContext context) { + Set lifecyclesInHierarchy = determineLifecyclesInTestClassHierarchy(Optional.of(context)); + return containsOnlyLifecyclePerMethod(lifecyclesInHierarchy); + } + + private static boolean containsOnlyLifecyclePerMethod(Set lifecyclesInHierarchy) { + return lifecyclesInHierarchy.size() == 1 && lifecyclesInHierarchy.contains(PER_METHOD); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static Set determineLifecyclesInTestClassHierarchy(Optional optionalContext) { + Set lifecycles = new HashSet<>(); + while (optionalContext.isPresent()) { + ExtensionContext currentContext = optionalContext.get(); + addLifecycleIfPresent(lifecycles, currentContext); + optionalContext = currentContext.getParent(); + } + return lifecycles; + } + + private static void addLifecycleIfPresent(Set lifecycles, ExtensionContext context) { + context.getTestInstanceLifecycle() + .ifPresent(lifecycles::add); + } + + private static boolean isNestedTestClass(ExtensionContext context) { + Optional parentContext = context.getParent(); + return parentContext.isPresent() && parentContext.get().getParent().isPresent(); + } } diff --git a/junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerClassNestedDemoTest.java b/junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerClassNestedDemoTest.java new file mode 100644 index 0000000..39973f0 --- /dev/null +++ b/junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerClassNestedDemoTest.java @@ -0,0 +1,127 @@ +package com.tngtech.valueprovider.nested; + +import java.util.ArrayList; +import java.util.List; + +import com.tngtech.valueprovider.ValueProvider; +import com.tngtech.valueprovider.ValueProviderAsserter; +import com.tngtech.valueprovider.ValueProviderExtension; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer.MethodName; +import org.junit.jupiter.api.extension.ExtendWith; + +import static com.tngtech.valueprovider.JUnit5Tests.ensureDefinedFactoryState; +import static com.tngtech.valueprovider.ValueProviderFactory.createRandomValueProvider; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; + +@TestInstance(PER_CLASS) +@DisplayName("Main test class, Lifecycle PER_CLASS") +@ExtendWith(ValueProviderExtension.class) +class LifecycleMainPerClassNestedDemoTest { + private static final ValueProvider classRandom; + + static { + ensureDefinedFactoryState(); + classRandom = createRandomValueProvider(); + } + + private final List randomsOfPreviousTestMethods = new ArrayList<>(); + private final ValueProvider mainInstanceRandom = createRandomValueProvider(); + private ValueProvider mainBeforeAllRandom; + private ValueProvider mainBeforeEachRandom; + + @BeforeAll + void mainBeforeAll() { + mainBeforeAllRandom = createRandomValueProvider(); + } + + @BeforeEach + void mainBeforeEach() { + mainBeforeEachRandom = createRandomValueProvider(); + } + + @Test + void should_ensure_reproducible_ValueProvider_creation_in_main_class() { + verifyReproducibleValueProviderCreationAndAdd(mainBeforeEachRandom, createRandomValueProvider()); + } + + @Test + void should_ensure_proper_separation_of_test_class_and_test_method_cycles_in_main_class() { + verifyReproducibleValueProviderCreationAndAdd(mainBeforeEachRandom, createRandomValueProvider()); + } + + @Nested + @TestInstance(PER_METHOD) + @DisplayName("Nested test class, Lifecycle PER_METHOD") + class LifecycleNestedPerMethod { + private final ValueProvider nestedInstanceRandom = createRandomValueProvider(); + private ValueProvider nestedBeforeEachRandom; + + @BeforeEach + void nestedBeforeEach() { + nestedBeforeEachRandom = createRandomValueProvider(); + } + + @Test + void should_ensure_reproducible_ValueProvider_creation_in_nested_class() { + verifyReproducibleValueProviderCreationAndAdd(nestedInstanceRandom, + mainBeforeEachRandom, nestedBeforeEachRandom, + createRandomValueProvider()); + } + + @Test + void should_ensure_proper_separation_of_test_class_and_test_method_cycles_in_nested_class() { + verifyReproducibleValueProviderCreationAndAdd(nestedInstanceRandom, + mainBeforeEachRandom, nestedBeforeEachRandom, + createRandomValueProvider()); + } + } + + @Nested + @TestInstance(PER_CLASS) + @TestMethodOrder(MethodName.class) + @DisplayName("Nested test class, Lifecycle PER_CLASS") + class LifecycleNestedPerClass { + private final ValueProvider nestedInstanceRandom = createRandomValueProvider(); + private ValueProvider nestedBeforeAllRandom; + private ValueProvider nestedBeforeEachRandom; + + @BeforeAll + void nestedBeforeAll() { + nestedBeforeAllRandom = createRandomValueProvider(); + } + + @BeforeEach + void nestedBeforeEach() { + nestedBeforeEachRandom = createRandomValueProvider(); + } + + @Test + void a_should_ensure_reproducible_ValueProvider_creation_in_nested_class() { + verifyReproducibleValueProviderCreationAndAdd(nestedInstanceRandom, nestedBeforeAllRandom, + mainBeforeEachRandom, nestedBeforeEachRandom, + createRandomValueProvider()); + } + + @Test + void b_should_ensure_proper_separation_of_test_class_and_test_method_cycles_in_nested_class() { + verifyReproducibleValueProviderCreationAndAdd(mainBeforeEachRandom, nestedBeforeEachRandom, + createRandomValueProvider()); + } + } + + private void verifyReproducibleValueProviderCreationAndAdd(ValueProvider... additionalTestMethodRandomValues) { + List additionalTestMethodRamdomValuesList = asList(additionalTestMethodRandomValues); + new ValueProviderAsserter() + .addExpectedTestClassRandomValues(classRandom) + .addExpectedTestMethodRandomValues(mainInstanceRandom, mainBeforeAllRandom) + .addExpectedTestMethodRandomValues(randomsOfPreviousTestMethods) + .addExpectedTestMethodRandomValues(additionalTestMethodRamdomValuesList) + .assertAllTestClassRandomValues() + .assertAllTestMethodRandomValues() + .assertAllSuffixes(); + randomsOfPreviousTestMethods.addAll(additionalTestMethodRamdomValuesList); + } +} diff --git a/junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerMethodNestedDemoTest.java b/junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerMethodNestedDemoTest.java new file mode 100644 index 0000000..9c05fe6 --- /dev/null +++ b/junit5/src/test/java/com/tngtech/valueprovider/nested/LifecycleMainPerMethodNestedDemoTest.java @@ -0,0 +1,150 @@ +package com.tngtech.valueprovider.nested; + +import java.util.ArrayList; +import java.util.List; + +import com.tngtech.valueprovider.RandomValues; +import com.tngtech.valueprovider.ValueProvider; +import com.tngtech.valueprovider.ValueProviderAsserter; +import com.tngtech.valueprovider.ValueProviderExtension; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtendWith; + +import static com.tngtech.valueprovider.JUnit5Tests.ensureDefinedFactoryState; +import static com.tngtech.valueprovider.ValueProviderFactory.createRandomValueProvider; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@DisplayName("Main test class, default Lifecycle PER_METHOD") +@ExtendWith(ValueProviderExtension.class) +class LifecycleMainPerMethodNestedDemoTest { + private static final ValueProvider classRandom; + + static { + ensureDefinedFactoryState(); + classRandom = createRandomValueProvider(); + } + + private static ValueProvider mainBeforeAllRandom; + private final ValueProvider mainInstanceRandom = createRandomValueProvider(); + private ValueProvider mainBeforeEachRandom; + + @BeforeAll + static void mainBeforeAll() { + mainBeforeAllRandom = createRandomValueProvider(); + } + + @BeforeEach + void mainBeforeEach() { + mainBeforeEachRandom = createRandomValueProvider(); + } + + @Test + void should_ensure_reproducible_ValueProvider_creation_in_main_class() { + verifyReproducibleValueProviderCreation(mainBeforeEachRandom, createRandomValueProvider()); + } + + @Test + void should_ensure_proper_separation_of_test_class_and_test_method_cycles_in_main_class() { + verifyReproducibleValueProviderCreation(mainBeforeEachRandom, createRandomValueProvider()); + } + + @Nested + @DisplayName("Nested test class (level 1), default Lifecycle PER_METHOD") + class LifecycleNestedLevel1PerMethod { + private final ValueProvider nestedLevel1InstanceRandom = createRandomValueProvider(); + private ValueProvider nestedLevel1BeforeEachRandom; + + @BeforeEach + void nestedLevel1BeforeEach() { + nestedLevel1BeforeEachRandom = createRandomValueProvider(); + } + + @Nested + @DisplayName("Nested test class (level 2), default Lifecycle PER_METHOD") + class LifecycleNestedLevel2PerMethod { + private final ValueProvider nestedLevel2InstanceRandom = createRandomValueProvider(); + private ValueProvider nestedLevel2BeforeEachRandom; + + @BeforeEach + void nestedLevel2BeforeEach() { + nestedLevel2BeforeEachRandom = createRandomValueProvider(); + } + + @Test + void should_ensure_reproducible_ValueProvider_creation_with_more_than_one_nesting_level() { + verifyReproducibleValueProviderCreation(nestedLevel1InstanceRandom, nestedLevel2InstanceRandom, + mainBeforeEachRandom, nestedLevel1BeforeEachRandom, nestedLevel2BeforeEachRandom, + createRandomValueProvider()); + } + + @Test + void should_ensure_proper_separation_of_test_class_and_test_method_cycles_with_more_than_one_nesting_level() { + verifyReproducibleValueProviderCreation(nestedLevel1InstanceRandom, nestedLevel2InstanceRandom, + mainBeforeEachRandom, nestedLevel1BeforeEachRandom, nestedLevel2BeforeEachRandom, + createRandomValueProvider()); + } + } + } + + private void verifyReproducibleValueProviderCreation(ValueProvider... additionalTestMethodRandomValues) { + new ValueProviderAsserter() + .addExpectedTestClassRandomValues(classRandom, mainBeforeAllRandom) + .addExpectedTestMethodRandomValues(mainInstanceRandom) + .addExpectedTestMethodRandomValues(asList(additionalTestMethodRandomValues)) + .assertAllTestClassRandomValues() + .assertAllTestMethodRandomValues() + .assertAllSuffixes(); + } + + @Nested + @TestInstance(PER_CLASS) + @DisplayName("Nested test class, Lifecycle PER_CLASS") + class LifecycleNestedPerClass { + private final List randomsOfPreviousTestMethods = new ArrayList<>(); + private final ValueProvider nestedInstanceRandom = createRandomValueProvider(); + private ValueProvider nestedBeforeAllRandom; + private ValueProvider nestedBeforeEachRandom; + + @BeforeAll + void nestedBeforeAll() { + nestedBeforeAllRandom = createRandomValueProvider(); + } + + @BeforeEach + void nestedBeforeEach() { + nestedBeforeEachRandom = createRandomValueProvider(); + } + + @Test + void should_ensure_reproducible_ValueProvider_creation_with_more_than_one_nesting_level() { + verifyReproducibleValueProviderCreation(mainBeforeEachRandom, nestedBeforeEachRandom, + createRandomValueProvider()); + } + + @Test + void should_ensure_proper_separation_of_test_class_and_test_method_cycles_in_nested_class() { + verifyReproducibleValueProviderCreation(mainBeforeEachRandom, nestedBeforeEachRandom, + createRandomValueProvider()); + } + + /** + * Note: {@link Lifecycle#PER_CLASS} requires separate verification method + * that adds the just verified {@link RandomValues}, as the test method cycle must not be reset + * before completion of the last test method. + */ + private void verifyReproducibleValueProviderCreation(ValueProvider... additionalTestMethodRandomValues) { + List additionalTestMethodRamdomValuesList = asList(additionalTestMethodRandomValues); + new ValueProviderAsserter() + .addExpectedTestClassRandomValues(classRandom, mainBeforeAllRandom) + .addExpectedTestMethodRandomValues(mainInstanceRandom, nestedInstanceRandom, nestedBeforeAllRandom) + .addExpectedTestMethodRandomValues(randomsOfPreviousTestMethods) + .addExpectedTestMethodRandomValues(additionalTestMethodRamdomValuesList) + .assertAllTestClassRandomValues() + .assertAllTestMethodRandomValues() + .assertAllSuffixes(); + randomsOfPreviousTestMethods.addAll(additionalTestMethodRamdomValuesList); + } + } +}