Skip to content

Commit 071ebaf

Browse files
authored
Add support for deterministic testing with JUnit (#1146)
1 parent f9d5704 commit 071ebaf

File tree

7 files changed

+217
-34
lines changed

7 files changed

+217
-34
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Fixture Monkey
3+
*
4+
* Copyright (c) 2021-present NAVER Corp.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.navercorp.fixturemonkey.api.engine;
20+
21+
import org.apiguardian.api.API;
22+
import org.apiguardian.api.API.Status;
23+
24+
/**
25+
* The {@link EngineUtils} class provides utility methods for engine.
26+
* Engine is a library that provides a way to generate random values.
27+
*/
28+
@API(since = "1.1.9", status = Status.EXPERIMENTAL)
29+
public abstract class EngineUtils {
30+
private static final boolean USE_JQWIK_ENGINE;
31+
private static final boolean USE_KOTEST_ENGINE;
32+
33+
static {
34+
boolean useJqwikEngine;
35+
boolean useKotestEngine;
36+
try {
37+
Class.forName("net.jqwik.engine.SourceOfRandomness");
38+
useJqwikEngine = true;
39+
} catch (ClassNotFoundException e) {
40+
useJqwikEngine = false;
41+
}
42+
USE_JQWIK_ENGINE = useJqwikEngine;
43+
44+
try {
45+
Class.forName("io.kotest.property.Arb");
46+
useKotestEngine = true;
47+
} catch (ClassNotFoundException e) {
48+
useKotestEngine = false;
49+
}
50+
USE_KOTEST_ENGINE = useKotestEngine;
51+
}
52+
53+
public static boolean useJqwikEngine() {
54+
return USE_JQWIK_ENGINE;
55+
}
56+
57+
public static boolean useKotestEngine() {
58+
return USE_KOTEST_ENGINE;
59+
}
60+
}

fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/random/Randoms.java

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,20 @@
2727

2828
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2929

30+
import com.navercorp.fixturemonkey.api.engine.EngineUtils;
31+
3032
/**
3133
* Reference jqwik SourceOfRandomness
3234
*/
3335
@API(since = "0.4.0", status = Status.INTERNAL)
3436
@SuppressFBWarnings("DMI_RANDOM_USED_ONLY_ONCE")
3537
public abstract class Randoms {
36-
private static final boolean USE_JQWIK_ENGINE;
3738
private static final ThreadLocal<Random> CURRENT;
3839
private static final ThreadLocal<Long> SEED;
3940

4041
static {
41-
boolean useJqwikEngine;
42-
try {
43-
Class.forName("net.jqwik.engine.SourceOfRandomness");
44-
useJqwikEngine = true;
45-
} catch (ClassNotFoundException e) {
46-
useJqwikEngine = false;
47-
}
48-
USE_JQWIK_ENGINE = useJqwikEngine;
4942
SEED = ThreadLocal.withInitial(System::nanoTime);
50-
CURRENT = ThreadLocal.withInitial(() -> Randoms.create(SEED.get()));
43+
CURRENT = ThreadLocal.withInitial(() -> Randoms.newGlobalSeed(SEED.get()));
5144
}
5245

5346
/**
@@ -60,12 +53,32 @@ public static Random create(String seed) {
6053
return CURRENT.get();
6154
}
6255

56+
/**
57+
* sets the initialized seed value.
58+
* If the seed has been initialized, it will no longer be changed.
59+
*
60+
* @param seed the seed value
61+
*/
6362
public static void setSeed(long seed) {
6463
SEED.set(seed);
6564
}
6665

66+
/**
67+
* Creates a new random instance with the given seed.
68+
* It affects the global seed value across multiple FixtureMonkey instances.
69+
* It is not recommended to use this method directly unless you intend to.
70+
* It is generally recommended to use {@link #setSeed(long)} instead.
71+
*
72+
* @param seed the seed value
73+
* @return a new random instance
74+
*/
75+
public static Random newGlobalSeed(long seed) {
76+
initializeGlobalSeed(seed);
77+
return CURRENT.get();
78+
}
79+
6780
public static Random current() {
68-
return USE_JQWIK_ENGINE
81+
return EngineUtils.useJqwikEngine()
6982
? SourceOfRandomness.current()
7083
: CURRENT.get();
7184
}
@@ -78,25 +91,26 @@ public static int nextInt(int bound) {
7891
return current().nextInt(bound);
7992
}
8093

81-
private static Random create(long seed) {
82-
if (USE_JQWIK_ENGINE) {
83-
SEED.set(seed);
84-
return SourceOfRandomness.create(String.valueOf(seed));
85-
}
86-
94+
/**
95+
* Creates a new random instance with the given seed. It is not thread safe.
96+
* It is generally recommended to use {@link #setSeed(long)} instead.
97+
* It affects the global seed value across multiple FixtureMonkey instances.
98+
*
99+
* @param seed the seed value
100+
*/
101+
private static void initializeGlobalSeed(long seed) {
87102
try {
88103
Random random = newRandom(seed);
89104
CURRENT.set(random);
90105
SEED.set(seed);
91-
return random;
92106
} catch (NumberFormatException nfe) {
93107
throw new IllegalArgumentException(String.format("[%s] is not a valid random seed.", seed));
94108
}
95109
}
96110

97111
private static Random newRandom(final long seed) {
98-
return USE_JQWIK_ENGINE
99-
? SourceOfRandomness.newRandom(seed)
112+
return EngineUtils.useJqwikEngine()
113+
? SourceOfRandomness.create(String.valueOf(seed))
100114
: new XorShiftRandom(seed);
101115
}
102116

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Fixture Monkey
3+
*
4+
* Copyright (c) 2021-present NAVER Corp.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.navercorp.fixturemonkey;
20+
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.concurrent.TimeUnit;
24+
25+
import org.openjdk.jmh.annotations.Benchmark;
26+
import org.openjdk.jmh.annotations.BenchmarkMode;
27+
import org.openjdk.jmh.annotations.Level;
28+
import org.openjdk.jmh.annotations.Mode;
29+
import org.openjdk.jmh.annotations.OutputTimeUnit;
30+
import org.openjdk.jmh.annotations.Scope;
31+
import org.openjdk.jmh.annotations.Setup;
32+
import org.openjdk.jmh.annotations.State;
33+
import org.openjdk.jmh.infra.Blackhole;
34+
35+
import com.navercorp.fixturemonkey.api.random.Randoms;
36+
import com.navercorp.fixturemonkey.api.type.TypeCache;
37+
import com.navercorp.fixturemonkey.javax.validation.plugin.JavaxValidationPlugin;
38+
39+
@BenchmarkMode(Mode.AverageTime)
40+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
41+
@State(Scope.Benchmark)
42+
public class SeedBenchmark {
43+
private static final int SEED = 1234;
44+
private static final int COUNT = 500;
45+
private static final FixtureMonkey SUT = FixtureMonkey.builder()
46+
.plugin(new JavaxValidationPlugin())
47+
.seed(SEED)
48+
.build();
49+
50+
@Setup(value = Level.Iteration)
51+
public void setUp() {
52+
TypeCache.clearCache();
53+
}
54+
55+
@Benchmark
56+
public void eachIterationCreatesNewJqwikSeed(Blackhole blackhole) throws Exception {
57+
Randoms.newGlobalSeed(SEED);
58+
blackhole.consume(generateOrderSheet());
59+
}
60+
61+
@Benchmark
62+
public void eachIterationNotCreatesNewJqwikSeed(Blackhole blackhole) throws Exception {
63+
blackhole.consume(generateOrderSheet());
64+
}
65+
66+
private List<OrderSheet> generateOrderSheet() {
67+
List<OrderSheet> result = new ArrayList<>();
68+
for (int i = 0; i < COUNT; i++) {
69+
result.add(SeedBenchmark.SUT.giveMeOne(OrderSheet.class));
70+
}
71+
return result;
72+
}
73+
}

fixture-monkey-junit-jupiter/build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
plugins {
2-
id("org.jetbrains.kotlin.jvm")
2+
id("org.jetbrains.kotlin.jvm")
33
id("com.navercorp.fixturemonkey.gradle.plugin.java-conventions")
44
id("com.navercorp.fixturemonkey.gradle.plugin.maven-publish-conventions")
55
}
@@ -16,5 +16,7 @@ dependencies {
1616
}
1717

1818
tasks.withType<Test> {
19-
useJUnitPlatform()
19+
useJUnitPlatform {
20+
includeEngines("junit-jupiter")
21+
}
2022
}

fixture-monkey-junit-jupiter/src/main/java/com/navercorp/fixturemonkey/junit/jupiter/extension/FixtureMonkeySeedExtension.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,30 @@
2828
import com.navercorp.fixturemonkey.api.random.Randoms;
2929
import com.navercorp.fixturemonkey.junit.jupiter.annotation.Seed;
3030

31+
/**
32+
* This extension sets the seed for generating random numbers before a test method is executed.
33+
* It also logs the seed used for the test if the test fails.
34+
* It aims to make the test deterministic and reproducible.
35+
* <p>
36+
* If the test method has a {@link Seed} annotation, it uses the value of the annotation as the seed.
37+
* If the test method does not have a {@link Seed} annotation, it uses the hash code of the test method as the seed.
38+
* <p>
39+
* The {@link Seed} annotation has a higher priority than the option {@code seed} in FixtureMonkey.
40+
*/
3141
public final class FixtureMonkeySeedExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
3242
private static final Logger LOGGER = LoggerFactory.getLogger(FixtureMonkeySeedExtension.class);
3343

3444
@Override
35-
public void beforeTestExecution(ExtensionContext context) throws Exception {
45+
public void beforeTestExecution(ExtensionContext context) {
3646
Seed seed = context.getRequiredTestMethod().getAnnotation(Seed.class);
3747
if (seed != null) {
3848
setSeed(seed.value());
49+
return;
3950
}
51+
52+
Method testMethod = context.getRequiredTestMethod();
53+
int methodHashCode = testMethod.hashCode();
54+
setSeed(methodHashCode);
4055
}
4156

4257
/**
@@ -45,7 +60,7 @@ public void beforeTestExecution(ExtensionContext context) throws Exception {
4560
* If the test failed, it logs the seed used for the test.
4661
**/
4762
@Override
48-
public void afterTestExecution(ExtensionContext context) throws Exception {
63+
public void afterTestExecution(ExtensionContext context) {
4964
if (context.getExecutionException().isPresent()) {
5065
logSeedIfTestFailed(context);
5166
}
@@ -55,7 +70,7 @@ public void afterTestExecution(ExtensionContext context) throws Exception {
5570
* Sets the seed for generating random numbers.
5671
**/
5772
private void setSeed(long seed) {
58-
Randoms.setSeed(seed);
73+
Randoms.newGlobalSeed(seed);
5974
}
6075

6176
/**

fixture-monkey-junit-jupiter/src/test/java/com/navercorp/fixturemonkey/junit/jupiter/extension/FixtureMonkeySeedExtensionTest.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.assertj.core.api.BDDAssertions.then;
2121

2222
import java.util.Arrays;
23+
import java.util.Collections;
2324
import java.util.HashSet;
2425
import java.util.List;
2526
import java.util.Set;
@@ -59,10 +60,7 @@ void latterValue() {
5960
@Seed(1)
6061
@RepeatedTest(100)
6162
void containerReturnsSame() {
62-
List<String> expected = Arrays.asList(
63-
"仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨婵얎⽒竻·俌欕悳잸횑ٻ킐結",
64-
"塸聩ዡ㘇뵥刲禮ᣮ鎊熇捺셾壍Ꜻꌩ垅凗❉償粐믩࠱哠"
65-
);
63+
List<String> expected = Collections.singletonList("仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨婵얎⽒竻·俌欕悳잸횑ٻ킐結");
6664

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

8076
Set<String> actual = SUT.giveMeOne(new TypeReference<Set<String>>() {
8177
});
@@ -94,4 +90,22 @@ void multipleContainerReturnsDiff() {
9490

9591
then(firstSet).isNotEqualTo(secondList);
9692
}
93+
94+
@Seed(1)
95+
@RepeatedTest(100)
96+
void multipleFixtureMonkeyInstancesReturnsAsOneInstance() {
97+
List<String> expected = Arrays.asList(
98+
"✠섨ꝓ仛禦催ᘓ蓊類౺阹瞻塢飖獾ࠒ⒐፨",
99+
"欕悳잸"
100+
);
101+
FixtureMonkey firstFixtureMonkey = FixtureMonkey.create();
102+
FixtureMonkey secondFixtureMonkey = FixtureMonkey.create();
103+
104+
List<String> actual = Arrays.asList(
105+
firstFixtureMonkey.giveMeOne(String.class),
106+
secondFixtureMonkey.giveMeOne(String.class)
107+
);
108+
109+
then(actual).isEqualTo(expected);
110+
}
97111
}

fixture-monkey/src/main/java/com/navercorp/fixturemonkey/FixtureMonkeyBuilder.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,14 @@ public FixtureMonkeyBuilder pushCustomizeValidOnly(TreeMatcher matcher, boolean
504504
}
505505

506506
/**
507-
* It is deprecated. Please use {@code @Seed} in fixture-monkey-junit-jupiter module.
507+
* sets the seed for generating random numbers.
508+
* <p>
509+
* If you use the {@code fixture-monkey-junit-jupiter} module,
510+
* the seed value can be overridden by the {@code Seed} annotation.
511+
*
512+
* @param seed seed value for generating random numbers.
513+
* @return FixtureMonkeyBuilder
508514
*/
509-
@Deprecated
510515
public FixtureMonkeyBuilder seed(long seed) {
511516
this.seed = seed;
512517
return this;

0 commit comments

Comments
 (0)