SharedRandomExtension.java
/**
* Copyright (C) 2022 Christopher J. Stehno
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.cjstehno.testthings.junit;
import io.github.cjstehno.testthings.rando.SharedRandom;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.support.ModifierSupport;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Optional;
import static io.github.cjstehno.testthings.rando.SharedRandom.current;
import static io.github.cjstehno.testthings.util.Reflections.extractValue;
import static java.lang.System.nanoTime;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN;
import static org.junit.platform.commons.support.ReflectionSupport.findFields;
/**
* A JUnit test extension used to test randomized scenarios in a way that removes the randomness. This extension will
* only work with classes that use the <code>SharedRandom</code> class to provide their randomization.
* <p>
* The extension will set a known seed value on the random generator so that it is no longer random. By default, a shared
* known seed will be used (see DEFAULT_KNOWN_SEED); however, this may be overridden by a configured value in your test
* class.
* <p>
* You can specify your own seed value by adding a field to your class with the signature
* <code>private static final long KNOWN_SEED = <your-value></code> to your test class. This provided value will
* be used instead of the default (it does not have to be "private").
* <p>
* Alternately, if your test method is annotated with the {@link ApplySeed} annotation, its value will be used as the
* seed for that test method.
* <p>
* The random generator is reset after each test by setting the seed to the current <code>nanoTime()</code> value (i.e.
* making it "random" again).
*
* <strong>Note:</strong> In case you are not aware, the seed-based random number generation is not really random - if
* you use the same seed, you get the same "random" values in the same order, which is the basis for this method of
* testing.
*/
@Slf4j
public class SharedRandomExtension implements BeforeEachCallback, AfterEachCallback {
/**
* The default known seed value - should be good enough for most cases. You can override this default by providing
* a <code>static final long KNOWN_SEED</code> field with your value.
*/
public static long DEFAULT_KNOWN_SEED = 4242424242L;
private static final String KNOWN_SEED = "KNOWN_SEED";
@Override public void beforeEach(final ExtensionContext context) throws Exception {
// set the seed to the default or configured value
((SharedRandom) current()).reseed(resolveKnownSeed(context.getRequiredTestClass(), context.getRequiredTestMethod()));
}
@Override public void afterEach(final ExtensionContext context) throws Exception {
// reset it to standard "random" behavior
((SharedRandom) current()).reseed(nanoTime());
}
private static long resolveKnownSeed(final Class<?> testClass, final Method testMethod) throws Exception {
var seed = findAnnotation(testMethod, ApplySeed.class).map(ApplySeed::value);
if (seed.isEmpty()) {
seed = firstField(testClass, KNOWN_SEED, Long.TYPE)
.map(f -> extractValue(testClass, f, Long.TYPE));
}
return seed.orElse(DEFAULT_KNOWN_SEED);
}
private static Optional<Field> firstField(final Class<?> testClass, final String fieldName, final Class<?> fieldType) {
return findFields(
testClass,
f -> ModifierSupport.isStatic(f) && f.getType().equals(fieldType) && f.getName().equals(fieldName),
TOP_DOWN
).stream().findFirst();
}
}