org.instancio.junit.InstancioExtension Maven / Gradle / Ivy
/*
* Copyright 2022-2024 the original author or authors.
*
* 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
*
* https://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 org.instancio.junit;
import org.instancio.junit.internal.ExtensionSupport;
import org.instancio.settings.Settings;
import org.instancio.support.DefaultRandom;
import org.instancio.support.ThreadLocalRandom;
import org.instancio.support.ThreadLocalSettings;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.Optional;
/**
* The Instancio JUnit extension adds support for additional
* features when using Instancio with JUnit Jupiter:
*
*
* - reporting the seed value to allow reproducing failed tests
* - injecting {@link Settings} using {@link WithSettings @WithSettings} annotation
* - generating parameterized test arguments using {@link InstancioSource @InstancioSource}
*
*
* Reproducing failed tests
*
* The extension generates a seed for each test method. When a test fails,
* the extension reports this seed in the output. Using the {@link Seed}
* annotation, the test can be re-run with the reported seed to reproduce
* the data that caused the failure.
*
*
For example, given the following test class:
*
*
* @ExtendWith(InstancioExtension.class)
* class ExampleTest {
*
* @Test
* void verifyPerson() {
* Person person = Instancio.create(Person.class);
* // some test code...
* // ... some assertion fails
* }
* }
*
*
* The failed test will report the seed value that was used, for example:
*
*
{@code "Test method 'verifyPerson' failed with seed: 12345"}
*
*
Subsequently, the failing test can be reproduced by annotating the test method
* with the {@link Seed} annotation:
*
*
* @Test
* @Seed(12345) // will reproduce previously generated data
* void verifyPerson() {
* Person person = Instancio.create(Person.class);
* // snip...
* }
*
*
* See the
* user guide
* for more details.
*
* @since 1.1.0
*/
public class InstancioExtension implements BeforeEachCallback, AfterEachCallback, AfterTestExecutionCallback {
private static final Logger LOG = LoggerFactory.getLogger(InstancioExtension.class);
private final ThreadLocalRandom threadLocalRandom;
private final ThreadLocalSettings threadLocalSettings;
/**
* Default constructor; required for JUnit extensions.
*/
@SuppressWarnings("unused")
public InstancioExtension() {
threadLocalRandom = ThreadLocalRandom.getInstance();
threadLocalSettings = ThreadLocalSettings.getInstance();
}
// Constructor used by unit test only
@SuppressWarnings("unused")
InstancioExtension(final ThreadLocalRandom threadLocalRandom,
final ThreadLocalSettings threadLocalSettings) {
this.threadLocalRandom = threadLocalRandom;
this.threadLocalSettings = threadLocalSettings;
}
@Override
public void beforeEach(final ExtensionContext context) {
ExtensionSupport.processAnnotations(context, threadLocalRandom, threadLocalSettings);
}
@Override
public void afterEach(final ExtensionContext context) {
threadLocalRandom.remove();
threadLocalSettings.remove();
}
@Override
public void afterTestExecution(final ExtensionContext context) {
if (context.getExecutionException().isPresent()) {
final Optional testMethod = context.getTestMethod();
if (!testMethod.isPresent()) {
return;
}
context.getStore(ExtensionContext.Namespace.create("x"));
// Should be safe to case. We don't expect any other implementations of Random.
final DefaultRandom random = (DefaultRandom) threadLocalRandom.get();
final long seed = random.getSeed();
final String seedMsg = String.format("Test method '%s' failed with seed: %d (seed source: %s)%n",
testMethod.get().getName(), seed, random.getSource().getDescription());
context.publishReportEntry("Instancio", seedMsg);
LOG.error(seedMsg);
}
}
}