com.github.robtimus.junit.support.extension.InjectingExtension Maven / Gradle / Ivy
Show all versions of junit-support Show documentation
/*
* InjectingExtension.java
* Copyright 2022 Rob Spoor
*
* 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 com.github.robtimus.junit.support.extension;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.commons.support.ModifierSupport;
import org.junit.platform.commons.support.ReflectionSupport;
/**
* An abstract base class for JUnit extensions that can inject values in fields and/or parameters.
*
* @author Rob Spoor
* @since 2.0
*/
public abstract class InjectingExtension implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {
private final Predicate fieldPredicate;
/**
* Creates a new extension.
*
* @param fieldPredicate A predicate that determines which fields are eligible for injection.
* @throws NullPointerException If the given predicate is {@code null}.
*/
protected InjectingExtension(Predicate fieldPredicate) {
this.fieldPredicate = Objects.requireNonNull(fieldPredicate);
}
@Override
public final void beforeAll(ExtensionContext context) throws Exception {
injectFields(null, context.getRequiredTestClass(), ModifierSupport::isStatic, context);
}
@Override
public final void beforeEach(ExtensionContext context) throws Exception {
for (Object testInstance : context.getRequiredTestInstances().getAllInstances()) {
injectFields(testInstance, testInstance.getClass(), ModifierSupport::isNotStatic, context);
}
}
private void injectFields(Object testInstance, Class> testClass, Predicate predicate, ExtensionContext context) {
for (Field field : ReflectionSupport.findFields(testClass, fieldPredicate.and(predicate), HierarchyTraversalMode.TOP_DOWN)) {
setValue(field, testInstance, context);
}
}
private void setValue(Field field, Object testInstance, ExtensionContext context) {
InjectionTarget target = InjectionTarget.forField(field);
validateTarget(target, context).ifPresent(e -> {
throw e;
});
try {
Object value = resolveValue(target, context);
if (!field.isAccessible()) {
field.setAccessible(true);
}
field.set(testInstance, value);
} catch (Exception e) {
throwAsUncheckedException(e);
}
}
@Override
public final boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
InjectionTarget target = InjectionTarget.forParameter(parameterContext);
return !validateTarget(target, extensionContext).isPresent();
}
/**
* Validates that a target is valid for injecting.
*
* If this method returns a non-empty {@link Optional} for parameter injection, {@link #supportsParameter(ParameterContext, ExtensionContext)}
* will return {@code false}, and JUnit will fail if no other extension supports the parameter.
*
* If this method returns a non-empty {@link Optional} for field injection, the exception is thrown. This situation may or may not be prevented
* using the field predicate used to create this extension. In some cases the predicate may not test all aspects that are used to inject a value
* into fields; if that's the case, throwing an error may be an appropriate action.
*
* @param target The target to validate; never {@code null}.
* @param context The current extension context; never {@code null}.
* @return {@link Optional#empty()} if the given target is valid for injecting, or an {@link Optional} describing an exception that indicates why
* the target is invalid otherwise. In that case, the exception should have been created using
* {@link InjectionTarget#createException(String)} or {@link InjectionTarget#createException(String, Throwable)}.
*/
protected abstract Optional validateTarget(InjectionTarget target, ExtensionContext context);
@Override
public final Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
InjectionTarget target = InjectionTarget.forParameter(parameterContext);
try {
return resolveValue(target, extensionContext);
} catch (Exception e) {
return throwAsUncheckedException(e);
}
}
/**
* Resolves the value to inject.
*
* When this method is called for parameter injection, {@link #supportsParameter(ParameterContext, ExtensionContext)} will have returned
* {@code true}, which means that {@link #validateTarget(InjectionTarget, ExtensionContext)} will have returned an empty {@link Optional}.
*
* When this method is called for field injection, {@link #validateTarget(InjectionTarget, ExtensionContext)} will have been called and verified
* to have returned an empty {@link Optional}.
*
* @param target The target to inject the value in; never {@code null}.
* @param context The current extension context; never {@code null}.
* @return The value to inject; possibly {@code null}.
* @throws Exception If the value could not be resolved.
*/
protected abstract Object resolveValue(InjectionTarget target, ExtensionContext context) throws Exception;
@SuppressWarnings("unchecked")
private static R throwAsUncheckedException(Throwable t) throws T {
throw (T) t;
}
}