Skip to content

Commit

Permalink
Add support for deterministic testing with JUnit
Browse files Browse the repository at this point in the history
  • Loading branch information
seongahjo committed Jan 18, 2025
1 parent 7a67237 commit 9f8474a
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@

package com.navercorp.fixturemonkey.api.random;

import java.util.Random;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.jqwik.engine.SourceOfRandomness;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;

import net.jqwik.engine.SourceOfRandomness;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Random;

/**
* Reference jqwik SourceOfRandomness
Expand All @@ -47,7 +45,7 @@ public abstract class Randoms {
}
USE_JQWIK_ENGINE = useJqwikEngine;
SEED = ThreadLocal.withInitial(System::nanoTime);
CURRENT = ThreadLocal.withInitial(() -> Randoms.create(SEED.get()));
CURRENT = ThreadLocal.withInitial(() -> Randoms.newGlobalSeed(SEED.get()));
}

/**
Expand All @@ -60,10 +58,30 @@ public static Random create(String seed) {
return CURRENT.get();
}

/**
* sets the initialized seed value.
* If the seed has been initialized, it will no longer be changed.
*
* @param seed the seed value
*/
public static void setSeed(long seed) {
SEED.set(seed);
}

/**
* Creates a new random instance with the given seed.
* It affects the global seed value across multiple FixtureMonkey instances.
* It is not recommended to use this method directly unless you intend to.
* It is generally recommended to use {@link #setSeed(long)} instead.
*
* @param seed the seed value
* @return a new random instance
*/
public static Random newGlobalSeed(long seed) {
initializeGlobalSeed(seed);
return CURRENT.get();
}

public static Random current() {
return USE_JQWIK_ENGINE
? SourceOfRandomness.current()
Expand All @@ -78,17 +96,24 @@ public static int nextInt(int bound) {
return current().nextInt(bound);
}

private static Random create(long seed) {
/**
* Creates a new random instance with the given seed. It is not thread safe.
* It is generally recommended to use {@link #setSeed(long)} instead.
* It affects the global seed value across multiple FixtureMonkey instances.
*
* @param seed the seed value
*/
private static void initializeGlobalSeed(long seed) {
if (USE_JQWIK_ENGINE) {
SEED.set(seed);
return SourceOfRandomness.create(String.valueOf(seed));
Random random = SourceOfRandomness.create(String.valueOf(seed));
CURRENT.set(random);
}

try {
Random random = newRandom(seed);
CURRENT.set(random);
SEED.set(seed);
return random;
} catch (NumberFormatException nfe) {
throw new IllegalArgumentException(String.format("[%s] is not a valid random seed.", seed));
}
Expand Down Expand Up @@ -127,7 +152,7 @@ private XorShiftRandom(long seed) {
protected int next(int nbits) {
long value = nextLong();
value &= ((1L << nbits) - 1);
return (int)value;
return (int) value;
}

/**
Expand Down
6 changes: 4 additions & 2 deletions fixture-monkey-junit-jupiter/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("org.jetbrains.kotlin.jvm")
id("org.jetbrains.kotlin.jvm")
id("com.navercorp.fixturemonkey.gradle.plugin.java-conventions")
id("com.navercorp.fixturemonkey.gradle.plugin.maven-publish-conventions")
}
Expand All @@ -16,5 +16,7 @@ dependencies {
}

tasks.withType<Test> {
useJUnitPlatform()
useJUnitPlatform {
includeEngines("junit-jupiter")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,38 @@
*/
package com.navercorp.fixturemonkey.junit.jupiter.extension;

import java.lang.reflect.Method;

import com.navercorp.fixturemonkey.api.random.Randoms;
import com.navercorp.fixturemonkey.junit.jupiter.annotation.Seed;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.navercorp.fixturemonkey.api.random.Randoms;
import com.navercorp.fixturemonkey.junit.jupiter.annotation.Seed;
import java.lang.reflect.Method;

/**
* This extension sets the seed for generating random numbers before a test method is executed.
* It also logs the seed used for the test if the test fails.
* It aims to make the test deterministic and reproducible.
* <p>
* If the test method has a {@link Seed} annotation, it uses the value of the annotation as the seed.
* If the test method does not have a {@link Seed} annotation, it uses the hash code of the test method as the seed.
*/
public final class FixtureMonkeySeedExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final Logger LOGGER = LoggerFactory.getLogger(FixtureMonkeySeedExtension.class);

@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
public void beforeTestExecution(ExtensionContext context) {
Seed seed = context.getRequiredTestMethod().getAnnotation(Seed.class);
if (seed != null) {
setSeed(seed.value());
return;
}

Method testMethod = context.getRequiredTestMethod();
int methodHashCode = testMethod.hashCode();
setSeed(methodHashCode);
}

/**
Expand All @@ -45,7 +57,7 @@ public void beforeTestExecution(ExtensionContext context) throws Exception {
* If the test failed, it logs the seed used for the test.
**/
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
public void afterTestExecution(ExtensionContext context) {
if (context.getExecutionException().isPresent()) {
logSeedIfTestFailed(context);
}
Expand All @@ -55,7 +67,7 @@ public void afterTestExecution(ExtensionContext context) throws Exception {
* Sets the seed for generating random numbers.
**/
private void setSeed(long seed) {
Randoms.setSeed(seed);
Randoms.newGlobalSeed(seed);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,15 @@
*/
package com.navercorp.fixturemonkey.junit.jupiter.extension;

import static org.assertj.core.api.BDDAssertions.then;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.extension.ExtendWith;

import com.navercorp.fixturemonkey.FixtureMonkey;
import com.navercorp.fixturemonkey.api.type.TypeReference;
import com.navercorp.fixturemonkey.junit.jupiter.annotation.Seed;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.*;

import static org.assertj.core.api.BDDAssertions.then;

@ExtendWith(FixtureMonkeySeedExtension.class)
class FixtureMonkeySeedExtensionTest {
Expand Down Expand Up @@ -59,10 +55,7 @@ void latterValue() {
@Seed(1)
@RepeatedTest(100)
void containerReturnsSame() {
List<String> expected = Arrays.asList(
"仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨婵얎⽒竻·俌欕悳잸횑ٻ킐結",
"塸聩ዡ㘇뵥刲禮ᣮ鎊熇捺셾壍Ꜻꌩ垅凗❉償粐믩࠱哠"
);
List<String> expected = Collections.singletonList("仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨婵얎⽒竻·俌欕悳잸횑ٻ킐結");

List<String> actual = SUT.giveMeOne(new TypeReference<List<String>>() {
});
Expand All @@ -73,9 +66,7 @@ void containerReturnsSame() {
@Seed(1)
@RepeatedTest(100)
void containerMattersOrder() {
Set<String> expected = new HashSet<>(
Arrays.asList("仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨婵얎⽒竻·俌欕悳잸횑ٻ킐結", "塸聩ዡ㘇뵥刲禮ᣮ鎊熇捺셾壍Ꜻꌩ垅凗❉償粐믩࠱哠")
);
Set<String> expected = new HashSet<>(Collections.singletonList("仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨婵얎⽒竻·俌欕悳잸횑ٻ킐結"));

Set<String> actual = SUT.giveMeOne(new TypeReference<Set<String>>() {
});
Expand All @@ -94,4 +85,22 @@ void multipleContainerReturnsDiff() {

then(firstSet).isNotEqualTo(secondList);
}

@Seed(1)
@RepeatedTest(100)
void multipleFixtureMonkeyInstancesReturnsAsOneInstance() {
List<String> expected = Arrays.asList(
"✠섨ꝓ仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨",
"欕悳잸"
);
FixtureMonkey firstFixtureMonkey = FixtureMonkey.create();
FixtureMonkey secondFixtureMonkey = FixtureMonkey.create();

List<String> actual = Arrays.asList(
firstFixtureMonkey.giveMeOne(String.class),
secondFixtureMonkey.giveMeOne(String.class)
);

then(actual).isEqualTo(expected);
}
}

0 comments on commit 9f8474a

Please sign in to comment.