org.mockito.internal.configuration.SpyAnnotationEngine Maven / Gradle / Ivy
Show all versions of mockito-core Show documentation
/*
* Copyright (c) 2007 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.internal.configuration;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.withSettings;
import static org.mockito.internal.exceptions.Reporter.unsupportedCombinationOfAnnotations;
import static org.mockito.internal.util.StringUtil.join;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.util.MockUtil;
import org.mockito.plugins.AnnotationEngine;
import org.mockito.plugins.MemberAccessor;
/**
* Process fields annotated with @Spy.
*
*
* Will try transform the field in a spy as with Mockito.spy()
.
*
*
*
* If the field is not initialized, will try to initialize it, with a no-arg constructor.
*
*
*
* If the field is also annotated with the compatible @InjectMocks then the field will be ignored,
* The injection engine will handle this specific case.
*
*
* This engine will fail, if the field is also annotated with incompatible Mockito annotations.
*/
@SuppressWarnings({"unchecked"})
public class SpyAnnotationEngine implements AnnotationEngine {
@Override
public AutoCloseable process(Class context, Object testInstance) {
Field[] fields = context.getDeclaredFields();
MemberAccessor accessor = Plugins.getMemberAccessor();
for (Field field : fields) {
if (field.isAnnotationPresent(Spy.class)
&& !field.isAnnotationPresent(InjectMocks.class)) {
assertNoIncompatibleAnnotations(Spy.class, field, Mock.class, Captor.class);
Object instance;
try {
instance = accessor.get(field, testInstance);
if (MockUtil.isMock(instance)) {
// instance has been spied earlier
// for example happens when MockitoAnnotations.openMocks is called two
// times.
Mockito.reset(instance);
} else if (instance != null) {
accessor.set(field, testInstance, spyInstance(field, instance));
} else {
accessor.set(field, testInstance, spyNewInstance(testInstance, field));
}
} catch (Exception e) {
throw new MockitoException(
"Unable to initialize @Spy annotated field '"
+ field.getName()
+ "'.\n"
+ e.getMessage(),
e);
}
}
}
return new NoAction();
}
private static Object spyInstance(Field field, Object instance) {
// TODO: Add mockMaker option for @Spy annotation (#2740)
return Mockito.mock(
instance.getClass(),
withSettings()
.genericTypeToMock(field.getGenericType())
.spiedInstance(instance)
.defaultAnswer(CALLS_REAL_METHODS)
.name(field.getName()));
}
private static Object spyNewInstance(Object testInstance, Field field)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
// TODO: Add mockMaker option for @Spy annotation (#2740)
MockSettings settings =
withSettings()
.genericTypeToMock(field.getGenericType())
.defaultAnswer(CALLS_REAL_METHODS)
.name(field.getName());
Class type = field.getType();
if (type.isInterface()) {
return Mockito.mock(type, settings.useConstructor());
}
int modifiers = type.getModifiers();
if (typeIsPrivateAbstractInnerClass(type, modifiers)) {
throw new MockitoException(
join(
"@Spy annotation can't initialize private abstract inner classes.",
" inner class: '" + type.getSimpleName() + "'",
" outer class: '" + type.getEnclosingClass().getSimpleName() + "'",
"",
"You should augment the visibility of this inner class"));
}
if (typeIsNonStaticInnerClass(type, modifiers)) {
Class enclosing = type.getEnclosingClass();
if (!enclosing.isInstance(testInstance)) {
throw new MockitoException(
join(
"@Spy annotation can only initialize inner classes declared in the test.",
" inner class: '" + type.getSimpleName() + "'",
" outer class: '" + enclosing.getSimpleName() + "'",
""));
}
return Mockito.mock(type, settings.useConstructor().outerInstance(testInstance));
}
Constructor constructor = noArgConstructorOf(type);
if (Modifier.isPrivate(constructor.getModifiers())) {
MemberAccessor accessor = Plugins.getMemberAccessor();
return Mockito.mock(type, settings.spiedInstance(accessor.newInstance(constructor)));
} else {
return Mockito.mock(type, settings.useConstructor());
}
}
private static Constructor noArgConstructorOf(Class type) {
Constructor constructor;
try {
constructor = type.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
throw new MockitoException(
"Please ensure that the type '"
+ type.getSimpleName()
+ "' has a no-arg constructor.",
e);
}
return constructor;
}
private static boolean typeIsNonStaticInnerClass(Class type, int modifiers) {
return !Modifier.isStatic(modifiers) && type.getEnclosingClass() != null;
}
private static boolean typeIsPrivateAbstractInnerClass(Class type, int modifiers) {
return Modifier.isPrivate(modifiers)
&& Modifier.isAbstract(modifiers)
&& type.getEnclosingClass() != null;
}
// TODO duplicated elsewhere
private static void assertNoIncompatibleAnnotations(
Class annotation,
Field field,
Class... undesiredAnnotations) {
for (Class u : undesiredAnnotations) {
if (field.isAnnotationPresent(u)) {
throw unsupportedCombinationOfAnnotations(
annotation.getSimpleName(), u.getSimpleName());
}
}
}
}