Skip to content

Commit

Permalink
add support for JUnit 5 @nested test classes
Browse files Browse the repository at this point in the history
Test with relevant combinations of Lifecycle PER_METHOD and PER_CLASS

Signed-off-by: Jonas Höf <[email protected]>
  • Loading branch information
jonashoef committed Nov 28, 2024
1 parent ed0e46e commit d504caa
Show file tree
Hide file tree
Showing 3 changed files with 366 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Consumer;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.TestInstance.Lifecycle;
Expand Down Expand Up @@ -45,13 +47,15 @@ public void beforeAll(ExtensionContext context) {
}

@Override
public <T> T interceptTestClassConstructor(Invocation<T> invocation, ReflectiveInvocationContext<Constructor<T>> invocationContext,
public <T> T interceptTestClassConstructor(
Invocation<T> invocation,
ReflectiveInvocationContext<Constructor<T>> invocationContext,
ExtensionContext extensionContext) throws Throwable {
logger.debug("{} interceptTestClassConstructor {}",
identityHashCode(this), buildQualifiedTestMethodName(extensionContext));
startTestClassCycleIf(extensionContext, PER_CLASS);
ensureStaticInitializationOfTestClass(extensionContext);
startTestMethodCycle();
startTestMethodCycle(extensionContext);
return invocation.proceed();
}

Expand Down Expand Up @@ -84,19 +88,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();
}
}
Expand All @@ -106,13 +114,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();
Expand All @@ -125,12 +139,6 @@ private void finishTestMethodCycleIfNecessary() {
}
}

private void finishTestMethodCycleIf(ExtensionContext context, Lifecycle lifecycle) {
if (isLifecycle(context, lifecycle)) {
finishTestMethodCycle();
}
}

private void finishTestMethodCycle() {
ValueProviderFactory.finishTestMethodCycle();
testMethodCycleState = CYCLE_COMLETED;
Expand Down Expand Up @@ -162,4 +170,71 @@ 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<Lifecycle> remainingLifecyclesInHierarchy = determineLifecyclesInTestClassHierarchy(context.getParent());
return remainingLifecyclesInHierarchy.isEmpty() || containsOnlyLifecyclePerMethod(remainingLifecyclesInHierarchy);
}

private static boolean testClassHierarchyHasOnlyLifecyclePerMethod(ExtensionContext context) {
Set<Lifecycle> lifecyclesInHierarchy = determineLifecyclesInTestClassHierarchy(Optional.of(context));
return containsOnlyLifecyclePerMethod(lifecyclesInHierarchy);
}

private static boolean containsOnlyLifecyclePerMethod(Set<Lifecycle> lifecyclesInHierarchy) {
return lifecyclesInHierarchy.size() == 1 && lifecyclesInHierarchy.contains(PER_METHOD);
}

@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static Set<Lifecycle> determineLifecyclesInTestClassHierarchy(Optional<ExtensionContext> optionalContext) {
Set<Lifecycle> lifecycles = new HashSet<>();
traverseContextHierarchy(optionalContext,
context -> {
Optional<Lifecycle> lifecycle = context.getTestInstanceLifecycle();
lifecycle.ifPresent(lifecycles::add);
}
);
return lifecycles;
}

private static boolean isNestedTestClass(ExtensionContext context) {
return determineNumTestClassesInHierarchy(context) > 1;
}

/**
* Cannot check e.g. for context class being org.junit.jupiter.engine.descriptor.ClassExtensionContext,
* as this class is package private. Only ClassExtensionContext instances seem to have a non-empty
* {@link Lifecycle} (tested via debugger for JUnit 5.10.2), however, so this is used as criteria instead.
* This seems reasonable, as the {@link Lifecycle} is exactly what controls instantiation of test classes.
*/
private static int determineNumTestClassesInHierarchy(ExtensionContext startContext) {
List<ExtensionContext> contextsWithLifecycle = new ArrayList<>();
traverseContextHierarchy(startContext,
context -> {
boolean hasLifecycle = context.getTestInstanceLifecycle().isPresent();
if (hasLifecycle) {
contextsWithLifecycle.add(context);
}
}
);
return contextsWithLifecycle.size();
}

private static void traverseContextHierarchy(ExtensionContext startContext,
Consumer<ExtensionContext> contextConsumer) {
traverseContextHierarchy(Optional.of(startContext), contextConsumer);
}

@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static void traverseContextHierarchy(Optional<ExtensionContext> optionalContext,
Consumer<ExtensionContext> contextConsumer) {
while (optionalContext.isPresent()) {
ExtensionContext context = optionalContext.get();
contextConsumer.accept(context);
optionalContext = context.getParent();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ValueProvider> 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<ValueProvider> additionalTestMethodRamdomValuesList = asList(additionalTestMethodRandomValues);
new ValueProviderAsserter()
.addExpectedTestClassRandomValues(classRandom)
.addExpectedTestMethodRandomValues(mainInstanceRandom, mainBeforeAllRandom)
.addExpectedTestMethodRandomValues(randomsOfPreviousTestMethods)
.addExpectedTestMethodRandomValues(additionalTestMethodRamdomValuesList)
.assertAllTestClassRandomValues()
.assertAllTestMethodRandomValues()
.assertAllSuffixes();
randomsOfPreviousTestMethods.addAll(additionalTestMethodRamdomValuesList);
}
}
Loading

0 comments on commit d504caa

Please sign in to comment.