Skip to content

Commit

Permalink
Merge pull request #43 from TNG/41-add-support-for-junit-5-lifecycle-…
Browse files Browse the repository at this point in the history
…per-class

Add support for JUnit 5 @nested test classes and Lifecycle PER_CLASS
  • Loading branch information
stefanhechtltng authored Dec 20, 2024
2 parents 6d6154d + 465be2c commit 942e94e
Show file tree
Hide file tree
Showing 25 changed files with 1,241 additions and 290 deletions.
215 changes: 154 additions & 61 deletions README.md

Large diffs are not rendered by default.

13 changes: 5 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,15 @@ ext {
dependency = [
apache_commons : [group: 'org.apache.commons', name: 'commons-lang3', version: '3.14.0'],
guava : [group: 'com.google.guava', name: 'guava', version: '33.0.0-jre'],
slf4j : [group: 'org.slf4j', name: 'slf4j-api', version: '2.0.12'],
slf4j_api : [group: 'org.slf4j', name: 'slf4j-api', version: '2.0.16'],
lombok : [group: 'org.projectlombok', name: 'lombok', version: '1.18.30'],

junit4 : [group: 'junit', name: 'junit', version: '4.13.2'],
junit4_dataprovider : [group: 'com.tngtech.junit.dataprovider', name: 'junit4-dataprovider', version: '2.10'],
junit5_dataprovider : [group: 'com.tngtech.junit.dataprovider', name: 'junit-jupiter-dataprovider', version: '2.10'],
assertj_core : [group: 'org.assertj', name: 'assertj-core', version: '3.25.3'],
mockito : [group: 'org.mockito', name: 'mockito-core', version: '5.10.0'],
log4j_api : [group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.23.0'],
log4j_core : [group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.23.0'],
log4j_slf4j : [group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.23.0'],
slf4j_simple : [group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.16'],

junit4_engine : [group: 'org.junit.vintage', name: 'junit-vintage-engine', version: '5.10.2'],
junit_jupiter_api : [group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.2'],
Expand Down Expand Up @@ -110,15 +108,14 @@ subprojects {
dependencies {
implementation dependency.apache_commons
implementation dependency.guava
implementation dependency.slf4j
implementation dependency.slf4j_api

testImplementation dependency.assertj_core
testImplementation dependency.junit4
testImplementation dependency.junit_jupiter_api

testRuntimeOnly dependency.log4j_slf4j
testRuntimeOnly dependency.log4j_api
testRuntimeOnly dependency.log4j_core
// hint: activate (debug) logging via system property -Dorg.slf4j.simpleLogger.defaultLogLevel=debug
testRuntimeOnly dependency.slf4j_simple
}

repositories {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
package com.tngtech.valueprovider;

import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_REFERENCE_DATE_TIME_PROPERTY;
import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_TEST_CLASS_SEED_PROPERTY;
import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_TEST_METHOD_SEED_PROPERTY;
import static com.tngtech.valueprovider.ValueProviderFactory.getFormattedReferenceDateTime;
import static com.tngtech.valueprovider.ValueProviderFactory.getTestClassSeed;
import static com.tngtech.valueprovider.ValueProviderFactory.getTestMethodSeed;
import java.util.Optional;

import com.google.common.annotations.VisibleForTesting;

import static com.tngtech.valueprovider.InitializationCreator.*;
import static com.tngtech.valueprovider.ValueProviderFactory.*;
import static java.lang.String.format;
import static java.util.Optional.empty;

@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class ValueProviderException extends RuntimeException {
ValueProviderException() {
super(provideFailureReproductionInfo());
this(empty());
}

ValueProviderException(Optional<Class<?>> testClassToReRunForReproduction) {
super(provideFailureReproductionInfo(testClassToReRunForReproduction));
}

static String provideFailureReproductionInfo() {
@VisibleForTesting
static String provideFailureReproductionInfo(Optional<Class<?>> testClassToReRunForReproduction) {
long testClassSeed = getTestClassSeed();
long testMethodSeed = getTestMethodSeed();
String referenceDateTime = getFormattedReferenceDateTime();
return format(
"If the failure is related to random ValueProviders, specify the following system properties for the JVM to reproduce:%n" +
"If the failure is related to random ValueProviders, %sspecify the following system properties for the JVM to reproduce:%n" +
"-D%s=%d%n" +
"-D%s=%d%n" +
"-D%s=%s",
formatReRunMessageFor(testClassToReRunForReproduction),
VALUE_PROVIDER_FACTORY_TEST_CLASS_SEED_PROPERTY, testClassSeed,
VALUE_PROVIDER_FACTORY_TEST_METHOD_SEED_PROPERTY, testMethodSeed,
VALUE_PROVIDER_FACTORY_REFERENCE_DATE_TIME_PROPERTY, referenceDateTime);
}

private static String formatReRunMessageFor(Optional<Class<?>> testClassToReRunForReproduction) {
return testClassToReRunForReproduction.map(testClass ->
format("re-run all tests of '%s' and ", testClass.getName()))
.orElse("");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -11,6 +12,7 @@
import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_TEST_CLASS_SEED_PROPERTY;
import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_TEST_METHOD_SEED_PROPERTY;
import static java.lang.System.setProperty;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;

/**
Expand Down Expand Up @@ -84,46 +86,47 @@ public ValueProviderAsserter assertAllSuffixes() {
}

private ValueProviderAsserter assertHasNextTestClassRandomValues(List<ValueProvider> providers) {
providers.forEach(this::assertHasNextTestClassRandomValues);
return this;
}

/**
* Note that this method is coupled to the implementation of {@link InitializationCreator} wrt. {@link RandomValues} creation.
* It does currently NOT support the extra cycles that may be required for unique VP suffixes,
* as those can easily be avoided be appropriate initial seed values, at least for a small number of created VPs.
*/
private ValueProviderAsserter assertHasNextTestClassRandomValues(ValueProvider provider) {
assertThat(provider.getRandom()).isEqualTo(expectedNextTestClassRandomValues());
return this;
return assertHasNextRandomValues(providers, "test-CLASS", this::expectedNextTestClassRandomValues);
}

private ValueProviderAsserter assertHasNextTestMethodRandomValues(List<ValueProvider> providers) {
providers.forEach(this::assertHasNextTestMethodRandomValues);
return this;
return assertHasNextRandomValues(providers, "test-METHOD", this::expectedNextTestMethodRandomValues);
}

/**
* @see #assertHasNextTestClassRandomValues(ValueProvider)
* Note that this method is coupled to the implementation of {@link InitializationCreator} wrt. {@link RandomValues} creation.
* It does currently NOT support the extra cycles that may be required for unique VP suffixes,
* as those can easily be avoided be appropriate initial seed values, at least for a small number of created VPs.
*/
@SuppressWarnings("UnusedReturnValue")
private ValueProviderAsserter assertHasNextTestMethodRandomValues(ValueProvider provider) {
assertThat(provider.getRandom()).isEqualTo(expectedNextTestMethodRandomValues());
private ValueProviderAsserter assertHasNextRandomValues(
List<ValueProvider> providers,
String testCycle, Supplier<RandomValues> expectedRandomValuesSupplier) {
List<RandomValues> expectedRandomValues = new ArrayList<>();
List<RandomValues> actualRandomValues = new ArrayList<>();
providers.forEach(provider -> {
actualRandomValues.add(provider.getRandom());
expectedRandomValues.add(expectedRandomValuesSupplier.get());
});
if (logger.isDebugEnabled()) {
List<Long> expectedSeeds = expectedRandomValues.stream()
.map(RandomValues::getSeed)
.collect(toList());
logger.debug("assertHasNextRandomValues({}), expectedRandomValues seeds {}", testCycle, expectedSeeds);
}
assertThat(actualRandomValues).as("%s random values", testCycle).isEqualTo(expectedRandomValues);
return this;
}

private RandomValues expectedNextTestClassRandomValues() {
return expectedNextRandomValues("test-CLASS", testClassSequence);
return expectedNextRandomValues(testClassSequence);
}

private RandomValues expectedNextTestMethodRandomValues() {
return expectedNextRandomValues("test-METHOD", testMethodSequence);
return expectedNextRandomValues(testMethodSequence);
}

private RandomValues expectedNextRandomValues(String testCycle, RandomValuesSequence sequence) {
RandomValues random = sequence.nextRandomValues();
logger.debug("expectedNextRandomValues({}), {}({})", testCycle, sequence.getSequenceCounter(), random.getSeed());
return random;
private RandomValues expectedNextRandomValues(RandomValuesSequence sequence) {
return sequence.nextRandomValues();
}

private ValueProviderAsserter assertSuffixes(Collection<ValueProvider> valueProviders) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.tngtech.valueprovider;

import java.util.Optional;

import org.junit.jupiter.api.Test;

import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_REFERENCE_DATE_TIME_PROPERTY;
import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_TEST_CLASS_SEED_PROPERTY;
import static com.tngtech.valueprovider.InitializationCreator.VALUE_PROVIDER_FACTORY_TEST_METHOD_SEED_PROPERTY;
import static com.tngtech.valueprovider.ValueProviderFactory.getFormattedReferenceDateTime;
import static com.tngtech.valueprovider.ValueProviderFactory.getTestClassSeed;
import static com.tngtech.valueprovider.ValueProviderFactory.getTestMethodSeed;
import static com.tngtech.valueprovider.InitializationCreator.*;
import static com.tngtech.valueprovider.ValueProviderFactory.*;
import static org.assertj.core.api.Assertions.assertThat;

class ValueProviderExceptionTest {
Expand All @@ -26,4 +24,14 @@ void should_show_seed_values_reference_date_time_and_respective_system_propertie
VALUE_PROVIDER_FACTORY_TEST_METHOD_SEED_PROPERTY,
VALUE_PROVIDER_FACTORY_REFERENCE_DATE_TIME_PROPERTY);
}

@Test
void should_show_test_class_to_re_run_for_failure_reproduction_if_provided() {
Class<?> testClassToReRun = this.getClass();
ValueProviderException exception = new ValueProviderException(Optional.of(testClassToReRun));

String message = exception.getMessage();

assertThat(message).contains(testClassToReRun.getName());
}
}
6 changes: 0 additions & 6 deletions core/src/test/resources/log4j2.xml

This file was deleted.

20 changes: 0 additions & 20 deletions core/src/test/resources/test-log4j2.xml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
@startuml
participant "JVM\nJUnit\nRuntime" as JVM
participant "Test A,\nLifecycle\n'PER_CLASS'" as T_A

participant "VPExtension\n(JUnit5)" as VPF_X #lightblue
participant "ValueProviderFactory" as VPF
note over VPF: Just shown for single thread,\nimplemented as ThreadLocal\nfor parallel test execution
participant "DefaultInitializationCreator" as D_VPC
note over D_VPC: 1. Uses arbitrary/random seed\n2. Does NOT ensure unique suffixes
participant "TestClassInitializationCreator" as C_VPC
participant "TestMethodInitializationCreator" as M_VPC
note over C_VPC, M_VPC: 1. Seed and reference date/time\n controllable via system properties\n2. Ensures unique suffixes\n (as far as possible)

JVM --> VPF : class-loading & instantiation
activate VPF
VPF -> D_VPC : instantiation
activate D_VPC
VPF -> M_VPC : instantiation
activate M_VPC
VPF -> VPF : activateCreator(DefaultInitializationCreator)

== Test A class-loading ==
JVM --> VPF_X : instantiation
activate VPF_X #lightblue
JVM -> VPF_X : beforeAll
note right #lightgreen: nothing to do for Lifecycle PER_CLASS
JVM -> VPF_X : interceptTestClassConstructor
VPF_X -> VPF : startTestClassCycle
VPF -> C_VPC : instantiation
activate C_VPC
note right of C_VPC #lightgreen: different lifecycle required\nfor tests with JUnit4 DataProvider
VPF -> C_VPC : startTestCycle
C_VPC -> C_VPC : initialize seed &\n reference date/time
note right of C_VPC #lightgreen: using system properties
VPF -> VPF : activateCreator(TestClassInitializationCreator)
VPF_X -> JVM : trigger class-loading of Test A

T_A -> VPF : createRandomValueProvider
note left #lightgreen: static initialization
VPF -> C_VPC : createRandomValueProvider

VPF_X -> VPF : startTestMethodCycle
VPF -> VPF : activateCreator(TestMethodInitializationCreator)
VPF -> M_VPC : startTestCycle(TestClassInitializationCreator)
M_VPC -> M_VPC : initialize seed &\n reference date/time
note right of M_VPC #lightgreen: using system properties
M_VPC -> M_VPC : copyValueProviderSuffixes(TestClassInitializationCreator)
note right of M_VPC #lightgreen: required to ensure unique suffixes
M_VPC -> M_VPC : copyReferenceDateTime(TestClassInitializationCreator)
note right of M_VPC #lightgreen: required to ensure reproducible\ndate/time related test data
JVM <-- VPF_X : proceed
== Test A test method <1> ==
JVM --> T_A : instantiation (once for all test methods)
activate T_A

T_A -> VPF : createRandomValueProvider
note left #lightgreen: instance variables
VPF -> M_VPC : createRandomValueProvider

T_A -> VPF : createRandomValueProvider
note left #lightgreen: before methods
VPF -> M_VPC : createRandomValueProvider

JVM -> T_A : test method <1>
T_A -> VPF : createRandomValueProvider
note left #lightgreen: test method
VPF -> M_VPC : createRandomValueProvider

T_A --> JVM : return
JVM -> VPF_X : afterEach
note right #lightgreen: nothing to do for Lifecycle PER_CLASS
newpage

== Test A test method <2> ==
T_A -> VPF : createRandomValueProvider
note left #lightgreen
(note: instance variables shared)
before methods
end note
VPF -> M_VPC : createRandomValueProvider

JVM -> T_A : test method <2>
T_A -> VPF : createRandomValueProvider
note left #lightgreen: test method
VPF -> M_VPC : createRandomValueProvider

T_A --> JVM : return
JVM -> VPF_X : afterEach
note right #lightgreen: nothing to do for Lifecycle PER_CLASS

JVM --> T_A : destruction
destroy T_A

JVM -> VPF_X : afterAll

VPF_X -> VPF : finishTestMethodCycle
VPF -> M_VPC : finishTestCycle
M_VPC -> M_VPC : resetValueProviderSuffixes
VPF -> VPF : activateCreator(TestClassInitializationCreator)

VPF_X -> VPF : finishTestClassCycle
VPF -> C_VPC : finishTestCycle
C_VPC -> C_VPC : resetValueProviderSuffixes
VPF -> VPF : delete TestClassInitializationCreator
destroy C_VPC
VPF -> VPF : activateCreator(DefaultInitializationCreator)

JVM --> VPF_X
destroy VPF_X

@enduml
Loading

0 comments on commit 942e94e

Please sign in to comment.