/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.junit.jupiter.engine.execution;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.function.Function;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.commons.util.ReflectionUtils;
import org.junit.platform.engine.ConfigurationParameters;
/**
* Unit tests for {@link ExecutableInvoker}.
*
* @since 5.0
*/
// @FullLogging(ExecutableInvoker.class)
class ExecutableInvokerTests {
private static final String ENIGMA = "enigma";
private final MethodSource instance = mock(MethodSource.class);
private Method method;
private final ExtensionContext extensionContext = mock(ExtensionContext.class);
private final ConfigurationParameters configParams = mock(ConfigurationParameters.class);
private ExtensionRegistry extensionRegistry = ExtensionRegistry.createRegistryWithDefaultExtensions(configParams);
@Test
void constructorInjection() throws Exception {
register(new StringParameterResolver(), new NumberParameterResolver());
Class<ConstructorInjectionTestCase> outerClass = ConstructorInjectionTestCase.class;
Constructor<ConstructorInjectionTestCase> constructor = ReflectionUtils.getDeclaredConstructor(outerClass);
ConstructorInjectionTestCase outer = newInvoker().invoke(constructor, extensionContext, extensionRegistry);
assertNotNull(outer);
assertEquals(ENIGMA, outer.str);
Class<ConstructorInjectionTestCase.NestedTestCase> innerClass = ConstructorInjectionTestCase.NestedTestCase.class;
Constructor<ConstructorInjectionTestCase.NestedTestCase> innerConstructor = ReflectionUtils.getDeclaredConstructor(
innerClass);
ConstructorInjectionTestCase.NestedTestCase inner = newInvoker().invoke(innerConstructor, outer,
extensionContext, extensionRegistry);
assertNotNull(inner);
assertEquals(42, inner.num);
}
@Test
void invokingMethodsWithoutParameterDoesNotDependOnExtensions() throws Exception {
testMethodWithNoParameters();
extensionRegistry = null;
invokeMethod();
verify(instance).noParameter();
}
@Test
void resolveArgumentsViaParameterResolver() {
testMethodWithASingleStringParameter();
thereIsAParameterResolverThatResolvesTheParameterTo("argument");
invokeMethod();
verify(instance).singleStringParameter("argument");
}
@Test
void resolveMultipleArguments() {
testMethodWith("multipleParameters", String.class, Integer.class, Double.class);
register(ConfigurableParameterResolver.supportsAndResolvesTo(parameterContext -> {
switch (parameterContext.getIndex()) {
case 0:
return "0";
case 1:
return 1;
default:
return 2.0;
}
}));
invokeMethod();
verify(instance).multipleParameters("0", 1, 2.0);
}
@Test
void onlyConsiderParameterResolversThatSupportAParticularParameter() {
testMethodWithASingleStringParameter();
thereIsAParameterResolverThatDoesNotSupportThisParameter();
thereIsAParameterResolverThatResolvesTheParameterTo("something");
invokeMethod();
verify(instance).singleStringParameter("something");
}
@Test
void passContextInformationToParameterResolverMethods() {
anyTestMethodWithAtLeastOneParameter();
ArgumentRecordingParameterResolver extension = new ArgumentRecordingParameterResolver();
register(extension);
invokeMethod();
assertSame(extensionContext, extension.supportsArguments.extensionContext);
assertEquals(0, extension.supportsArguments.parameterContext.getIndex());
assertSame(instance, extension.supportsArguments.parameterContext.getTarget().get());
assertSame(extensionContext, extension.resolveArguments.extensionContext);
assertEquals(0, extension.resolveArguments.parameterContext.getIndex());
assertSame(instance, extension.resolveArguments.parameterContext.getTarget().get());
}
@Test
void invocationOfMethodsWithPrimitiveTypesIsSupported() {
testMethodWithASinglePrimitiveIntParameter();
thereIsAParameterResolverThatResolvesTheParameterTo(42);
invokeMethod();
verify(instance).primitiveParameterInt(42);
}
@Test
void nullIsAViableArgumentIfAReferenceTypeParameterIsExpected() {
testMethodWithASingleStringParameter();
thereIsAParameterResolverThatResolvesTheParameterTo(null);
invokeMethod();
verify(instance).singleStringParameter(null);
}
@Test
void reportThatNullIsNotAViableArgumentIfAPrimitiveTypeIsExpected() {
testMethodWithASinglePrimitiveIntParameter();
thereIsAParameterResolverThatResolvesTheParameterTo(null);
ParameterResolutionException caught = assertThrows(ParameterResolutionException.class, this::invokeMethod);
// @formatter:off
assertThat(caught.getMessage())
.contains("resolved a null value for parameter [int")
.contains("but a primitive of type [int] is required.");
// @formatter:on
}
@Test
void reportIfThereIsNoParameterResolverThatSupportsTheParameter() {
testMethodWithASingleStringParameter();
ParameterResolutionException caught = assertThrows(ParameterResolutionException.class, this::invokeMethod);
assertThat(caught.getMessage()).contains("parameter [java.lang.String");
}
@Test
void reportIfThereAreMultipleParameterResolversThatSupportTheParameter() {
testMethodWithASingleStringParameter();
thereIsAParameterResolverThatResolvesTheParameterTo("one");
thereIsAParameterResolverThatResolvesTheParameterTo("two");
ParameterResolutionException caught = assertThrows(ParameterResolutionException.class, this::invokeMethod);
// @formatter:off
assertThat(caught.getMessage())
.contains("parameter [java.lang.String")
.contains(ConfigurableParameterResolver.class.getName() + ", " + ConfigurableParameterResolver.class.getName());
// @formatter:on
}
@Test
void reportTypeMismatchBetweenParameterAndResolvedParameter() {
testMethodWithASingleStringParameter();
thereIsAParameterResolverThatResolvesTheParameterTo(BigDecimal.ONE);
ParameterResolutionException caught = assertThrows(ParameterResolutionException.class, this::invokeMethod);
// @formatter:off
assertThat(caught.getMessage())
.contains("resolved a value of type [java.math.BigDecimal] for parameter [java.lang.String")
.contains("but a value assignment compatible with [java.lang.String] is required.");
// @formatter:on
}
@Test
void wrapAllExceptionsThrownDuringParameterResolutionIntoAParameterResolutionException() {
anyTestMethodWithAtLeastOneParameter();
IllegalArgumentException cause = anyExceptionButParameterResolutionException();
throwDuringParameterResolution(cause);
ParameterResolutionException caught = assertThrows(ParameterResolutionException.class, this::invokeMethod);
assertSame(cause, caught.getCause(), () -> "cause should be present");
assertThat(caught.getMessage()).startsWith("Failed to resolve parameter [java.lang.String");
}
@Test
void doNotWrapThrownExceptionIfItIsAlreadyAParameterResolutionException() {
anyTestMethodWithAtLeastOneParameter();
ParameterResolutionException cause = new ParameterResolutionException("custom message");
throwDuringParameterResolution(cause);
ParameterResolutionException caught = assertThrows(ParameterResolutionException.class, this::invokeMethod);
assertSame(cause, caught);
}
private IllegalArgumentException anyExceptionButParameterResolutionException() {
return new IllegalArgumentException();
}
private void throwDuringParameterResolution(RuntimeException parameterResolutionException) {
register(ConfigurableParameterResolver.onAnyCallThrow(parameterResolutionException));
}
private void thereIsAParameterResolverThatResolvesTheParameterTo(Object argument) {
register(ConfigurableParameterResolver.supportsAndResolvesTo(parameterContext -> argument));
}
private void thereIsAParameterResolverThatDoesNotSupportThisParameter() {
register(ConfigurableParameterResolver.withoutSupport());
}
private void anyTestMethodWithAtLeastOneParameter() {
testMethodWithASingleStringParameter();
}
private void testMethodWithNoParameters() {
testMethodWith("noParameter");
}
private void testMethodWithASingleStringParameter() {
testMethodWith("singleStringParameter", String.class);
}
private void testMethodWithASinglePrimitiveIntParameter() {
testMethodWith("primitiveParameterInt", int.class);
}
private void testMethodWith(String methodName, Class<?>... parameterTypes) {
this.method = ReflectionUtils.findMethod(this.instance.getClass(), methodName, parameterTypes).get();
}
private void register(ParameterResolver... resolvers) {
for (ParameterResolver resolver : resolvers) {
extensionRegistry.registerExtension(resolver, this);
}
}
private ExecutableInvoker newInvoker() {
return new ExecutableInvoker();
}
private void invokeMethod() {
newInvoker().invoke(this.method, this.instance, this.extensionContext, this.extensionRegistry);
}
// -------------------------------------------------------------------------
static class ArgumentRecordingParameterResolver implements ParameterResolver {
Arguments supportsArguments;
Arguments resolveArguments;
static class Arguments {
final ParameterContext parameterContext;
final ExtensionContext extensionContext;
Arguments(ParameterContext parameterContext, ExtensionContext extensionContext) {
this.parameterContext = parameterContext;
this.extensionContext = extensionContext;
}
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
supportsArguments = new Arguments(parameterContext, extensionContext);
return true;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
resolveArguments = new Arguments(parameterContext, extensionContext);
return null;
}
}
static class ConfigurableParameterResolver implements ParameterResolver {
static ParameterResolver onAnyCallThrow(RuntimeException runtimeException) {
return new ConfigurableParameterResolver(parameterContext -> {
throw runtimeException;
}, parameterContext -> {
throw runtimeException;
});
}
static ParameterResolver supportsAndResolvesTo(Function<ParameterContext, Object> resolve) {
return new ConfigurableParameterResolver(parameterContext -> true, resolve);
}
static ParameterResolver withoutSupport() {
return new ConfigurableParameterResolver(parameterContext -> false, parameter -> {
throw new UnsupportedOperationException();
});
}
private final Predicate<ParameterContext> supports;
private final Function<ParameterContext, Object> resolve;
private ConfigurableParameterResolver(Predicate<ParameterContext> supports,
Function<ParameterContext, Object> resolve) {
this.supports = supports;
this.resolve = resolve;
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return supports.test(parameterContext);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return resolve.apply(parameterContext);
}
}
interface MethodSource {
void noParameter();
void singleStringParameter(String parameter);
void primitiveParameterInt(int parameter);
void multipleParameters(String first, Integer second, Double third);
}
private static class StringParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == String.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return ENIGMA;
}
}
private static class NumberParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == Number.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 42;
}
}
private static class ConstructorInjectionTestCase {
final String str;
@SuppressWarnings("unused")
ConstructorInjectionTestCase(String str) {
this.str = str;
}
class NestedTestCase {
final Number num;
@SuppressWarnings("unused")
NestedTestCase(Number num) {
this.num = num;
}
}
}
}