Skip to content

Commit 4a8d01a

Browse files
committed
Add support for deterministic testing with JUnit
1 parent 7a67237 commit 4a8d01a

File tree

6 files changed

+159
-28
lines changed

6 files changed

+159
-28
lines changed

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public abstract class Randoms {
4747
}
4848
USE_JQWIK_ENGINE = useJqwikEngine;
4949
SEED = ThreadLocal.withInitial(System::nanoTime);
50-
CURRENT = ThreadLocal.withInitial(() -> Randoms.create(SEED.get()));
50+
CURRENT = ThreadLocal.withInitial(() -> Randoms.newGlobalSeed(SEED.get()));
5151
}
5252

5353
/**
@@ -60,10 +60,30 @@ public static Random create(String seed) {
6060
return CURRENT.get();
6161
}
6262

63+
/**
64+
* sets the initialized seed value.
65+
* If the seed has been initialized, it will no longer be changed.
66+
*
67+
* @param seed the seed value
68+
*/
6369
public static void setSeed(long seed) {
6470
SEED.set(seed);
6571
}
6672

73+
/**
74+
* Creates a new random instance with the given seed.
75+
* It affects the global seed value across multiple FixtureMonkey instances.
76+
* It is not recommended to use this method directly unless you intend to.
77+
* It is generally recommended to use {@link #setSeed(long)} instead.
78+
*
79+
* @param seed the seed value
80+
* @return a new random instance
81+
*/
82+
public static Random newGlobalSeed(long seed) {
83+
initializeGlobalSeed(seed);
84+
return CURRENT.get();
85+
}
86+
6787
public static Random current() {
6888
return USE_JQWIK_ENGINE
6989
? SourceOfRandomness.current()
@@ -78,17 +98,24 @@ public static int nextInt(int bound) {
7898
return current().nextInt(bound);
7999
}
80100

81-
private static Random create(long seed) {
101+
/**
102+
* Creates a new random instance with the given seed. It is not thread safe.
103+
* It is generally recommended to use {@link #setSeed(long)} instead.
104+
* It affects the global seed value across multiple FixtureMonkey instances.
105+
*
106+
* @param seed the seed value
107+
*/
108+
private static void initializeGlobalSeed(long seed) {
82109
if (USE_JQWIK_ENGINE) {
83110
SEED.set(seed);
84-
return SourceOfRandomness.create(String.valueOf(seed));
111+
Random random = SourceOfRandomness.create(String.valueOf(seed));
112+
CURRENT.set(random);
85113
}
86114

87115
try {
88116
Random random = newRandom(seed);
89117
CURRENT.set(random);
90118
SEED.set(seed);
91-
return random;
92119
} catch (NumberFormatException nfe) {
93120
throw new IllegalArgumentException(String.format("[%s] is not a valid random seed.", seed));
94121
}
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: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,15 @@
1717
*/
1818
package com.navercorp.fixturemonkey.junit.jupiter.extension;
1919

20-
import static org.assertj.core.api.BDDAssertions.then;
21-
22-
import java.util.Arrays;
23-
import java.util.HashSet;
24-
import java.util.List;
25-
import java.util.Set;
26-
27-
import org.junit.jupiter.api.RepeatedTest;
28-
import org.junit.jupiter.api.extension.ExtendWith;
29-
3020
import com.navercorp.fixturemonkey.FixtureMonkey;
3121
import com.navercorp.fixturemonkey.api.type.TypeReference;
3222
import com.navercorp.fixturemonkey.junit.jupiter.annotation.Seed;
23+
import org.junit.jupiter.api.RepeatedTest;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
26+
import java.util.*;
27+
28+
import static org.assertj.core.api.BDDAssertions.then;
3329

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

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

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

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

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)