Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JUnit 5 @Nested test classes and Lifecycle PER_CLASS #43

Merged
merged 10 commits into from
Dec 20, 2024
Merged
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
Loading