Skip to content

Commit

Permalink
Rework spying on concrete instances
Browse files Browse the repository at this point in the history
  • Loading branch information
leonard84 committed Dec 25, 2017
1 parent c60373c commit 38c58e2
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 85 deletions.
@@ -1,11 +1,13 @@
package org.spockframework.mock;

import org.spockframework.util.*;
import spock.mock.MockingApi;

import java.lang.reflect.Type;
import java.util.List;

import org.spockframework.util.Beta;
import org.spockframework.util.Nullable;

import spock.mock.MockingApi;

/**
* Configuration options for mock objects. Once a mock object has been created, its configuration cannot be changed.
*
Expand Down Expand Up @@ -35,9 +37,9 @@ public interface IMockConfiguration {
Class<?> getType();

/**
* Returns the instance to be used as a delegate
* Returns the instance to be used as a prototype
*
* @return the instance to be used as a delegate
* @return the instance to be used as a prototype
*/
@Nullable
Object getInstance();
Expand Down
Expand Up @@ -14,13 +14,13 @@

package org.spockframework.mock;

import java.lang.reflect.Type;

import org.spockframework.mock.runtime.SpecificationAttachable;
import org.spockframework.util.Nullable;

import spock.lang.Specification;

import java.lang.reflect.Type;

public interface IMockObject extends SpecificationAttachable {
/**
* Returns the name of this mock object, or {@code null} if it has no name.
Expand Down Expand Up @@ -51,13 +51,6 @@ public interface IMockObject extends SpecificationAttachable {
* @return the instance of this mock object
*/
Object getInstance();

/**
* Returns the original instance provided by the user for wrapping with a Spy, or {@code null} if not applicable.
*
* @return the the original instance provided by the user for wrapping with a Spy, or {@code null} if not applicable.
*/
Object getUserCreatedInstance();

/**
* Tells whether this mock object supports verification of invocations.
Expand Down
@@ -1,10 +1,8 @@
package org.spockframework.mock.runtime;

import org.spockframework.mock.IMockInvocation;
import org.spockframework.mock.IMockMethod;
import org.spockframework.mock.IResponseGenerator;
import org.spockframework.util.ExceptionUtil;
import org.spockframework.util.ReflectionUtil;

public class ByteBuddyMethodInvoker implements IResponseGenerator {

Expand All @@ -20,12 +18,6 @@ public Object respond(IMockInvocation invocation) {
throw new IllegalStateException("Cannot invoke abstract method " + invocation.getMethod());
}
try {
Object userCreatedInstance = invocation.getMockObject().getUserCreatedInstance();
if(userCreatedInstance != null) {
IMockMethod method = invocation.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes().toArray(new Class<?>[method.getParameterTypes().size()]);
return ReflectionUtil.getMethodBySignature(userCreatedInstance.getClass(), method.getName(), parameterTypes).invoke(userCreatedInstance, invocation.getArguments().toArray());
}
return superCall.call(invocation.getArguments().toArray());
} catch (Throwable t) {
// Byte Buddy doesn't wrap exceptions in InvocationTargetException, so no need to unwrap
Expand Down
Expand Up @@ -14,7 +14,8 @@

package org.spockframework.mock.runtime;

import org.spockframework.mock.*;
import org.spockframework.mock.IMockInvocation;
import org.spockframework.mock.IResponseGenerator;
import org.spockframework.util.ExceptionUtil;

import net.sf.cglib.proxy.MethodProxy;
Expand All @@ -29,11 +30,7 @@ public CglibRealMethodInvoker(MethodProxy methodProxy) {
@Override
public Object respond(IMockInvocation invocation) {
try {
if(invocation.getMockObject().getUserCreatedInstance() != null) {
return methodProxy.invoke(invocation.getMockObject().getUserCreatedInstance(), invocation.getArguments().toArray());
} else {
return methodProxy.invokeSuper(invocation.getMockObject().getInstance(), invocation.getArguments().toArray());
}
return methodProxy.invokeSuper(invocation.getMockObject().getInstance(), invocation.getArguments().toArray());
} catch (Throwable t) {
// MethodProxy doesn't wrap exceptions in InvocationTargetException, so no need to unwrap
ExceptionUtil.sneakyThrow(t);
Expand Down
Expand Up @@ -14,14 +14,18 @@

package org.spockframework.mock.runtime;

import org.spockframework.mock.*;
import org.spockframework.util.*;
import spock.lang.Specification;
import java.util.Arrays;
import java.util.List;

import org.spockframework.mock.IMockConfiguration;
import org.spockframework.mock.IMockFactory;
import org.spockframework.util.InternalSpockError;
import org.spockframework.util.UnreachableCodeError;

import java.util.*;
import spock.lang.Specification;

public class CompositeMockFactory implements IMockFactory {
public static CompositeMockFactory INSTANCE =
public static final CompositeMockFactory INSTANCE =
new CompositeMockFactory(Arrays.asList(JavaMockFactory.INSTANCE, GroovyMockFactory.INSTANCE));

private final List<IMockFactory> mockFactories;
Expand Down
Expand Up @@ -14,17 +14,24 @@

package org.spockframework.mock.runtime;

import org.spockframework.mock.*;
import org.spockframework.runtime.GroovyRuntimeUtil;
import spock.lang.Specification;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import groovy.lang.*;
import org.spockframework.mock.CannotCreateMockException;
import org.spockframework.mock.IMockConfiguration;
import org.spockframework.mock.IMockFactory;
import org.spockframework.mock.MockImplementation;
import org.spockframework.mock.MockNature;
import org.spockframework.runtime.GroovyRuntimeUtil;
import org.spockframework.util.ReflectionUtil;

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import spock.lang.Specification;

public class GroovyMockFactory implements IMockFactory {
public static GroovyMockFactory INSTANCE = new GroovyMockFactory();
public static final GroovyMockFactory INSTANCE = new GroovyMockFactory();

@Override
public boolean canCreate(IMockConfiguration configuration) {
Expand Down Expand Up @@ -69,11 +76,15 @@ public void run() {
}

IProxyBasedMockInterceptor mockInterceptor = new GroovyMockInterceptor(configuration, specification, newMetaClass);
ArrayList<Class<?>> additionalInterfaces = new ArrayList<>(configuration.getAdditionalInterfaces());
List<Class<?>> additionalInterfaces = new ArrayList<>(configuration.getAdditionalInterfaces());
additionalInterfaces.add(GroovyObject.class);
return ProxyBasedMockFactory.INSTANCE.create(type, additionalInterfaces,
configuration.getConstructorArgs(), mockInterceptor, specification.getClass().getClassLoader(),
configuration.isUseObjenesis());
Object proxy = ProxyBasedMockFactory.INSTANCE.create(type, additionalInterfaces,
configuration.getConstructorArgs(), mockInterceptor, specification.getClass().getClassLoader(),
configuration.isUseObjenesis());
if ((configuration.getNature() == MockNature.SPY) && (configuration.getInstance() != null)) {
ReflectionUtil.deepCopyFields(configuration.getInstance(), proxy);
}
return proxy;
}

private boolean isFinalClass(Class<?> type) {
Expand Down
Expand Up @@ -16,16 +16,21 @@

package org.spockframework.mock.runtime;

import org.spockframework.mock.*;
import org.spockframework.runtime.GroovyRuntimeUtil;
import spock.lang.Specification;

import java.lang.reflect.Modifier;

import org.spockframework.mock.CannotCreateMockException;
import org.spockframework.mock.IMockConfiguration;
import org.spockframework.mock.IMockFactory;
import org.spockframework.mock.MockImplementation;
import org.spockframework.mock.MockNature;
import org.spockframework.runtime.GroovyRuntimeUtil;
import org.spockframework.util.ReflectionUtil;

import groovy.lang.MetaClass;
import spock.lang.Specification;

public class JavaMockFactory implements IMockFactory {
public static JavaMockFactory INSTANCE = new JavaMockFactory();
public static final JavaMockFactory INSTANCE = new JavaMockFactory();

@Override
public boolean canCreate(IMockConfiguration configuration) {
Expand Down Expand Up @@ -54,9 +59,13 @@ private Object createInternal(IMockConfiguration configuration, Specification sp

MetaClass mockMetaClass = GroovyRuntimeUtil.getMetaClass(configuration.getType());
IProxyBasedMockInterceptor interceptor = new JavaMockInterceptor(configuration, specification, mockMetaClass);
return ProxyBasedMockFactory.INSTANCE.create(configuration.getType(), configuration.getAdditionalInterfaces(),
configuration.getConstructorArgs(), interceptor, classLoader,
configuration.isUseObjenesis());
Object proxy = ProxyBasedMockFactory.INSTANCE.create(configuration.getType(), configuration.getAdditionalInterfaces(),
configuration.getConstructorArgs(), interceptor, classLoader,
configuration.isUseObjenesis());
if ((configuration.getNature() == MockNature.SPY) && (configuration.getInstance() != null)) {
ReflectionUtil.deepCopyFields(configuration.getInstance(), proxy);
}
return proxy;
}
}

Expand Down
Expand Up @@ -14,14 +14,21 @@

package org.spockframework.mock.runtime;

import org.spockframework.mock.*;
import org.spockframework.runtime.GroovyRuntimeUtil;
import spock.lang.Specification;

import java.lang.reflect.Method;
import java.util.Arrays;

import groovy.lang.*;
import org.spockframework.mock.IMockConfiguration;
import org.spockframework.mock.IMockController;
import org.spockframework.mock.IMockInvocation;
import org.spockframework.mock.IMockMethod;
import org.spockframework.mock.IMockObject;
import org.spockframework.mock.IResponseGenerator;
import org.spockframework.mock.ISpockMockObject;
import org.spockframework.runtime.GroovyRuntimeUtil;

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import spock.lang.Specification;

public class JavaMockInterceptor implements IProxyBasedMockInterceptor {
private final IMockConfiguration mockConfiguration;
Expand All @@ -38,7 +45,7 @@ public JavaMockInterceptor(IMockConfiguration mockConfiguration, Specification s
@Override
public Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker) {
IMockObject mockObject = new MockObject(mockConfiguration.getName(), mockConfiguration.getExactType(),
target, mockConfiguration.getInstance(), mockConfiguration.isVerified(), false, mockConfiguration.getDefaultResponse(), specification, this);
target, mockConfiguration.isVerified(), false, mockConfiguration.getDefaultResponse(), specification, this);

if (method.getDeclaringClass() == ISpockMockObject.class) {
return mockObject;
Expand Down
Expand Up @@ -14,20 +14,22 @@

package org.spockframework.mock.runtime;

import java.lang.reflect.Type;

import org.spockframework.gentyref.GenericTypeReflector;
import org.spockframework.lang.Wildcard;
import org.spockframework.mock.*;
import org.spockframework.mock.IDefaultResponse;
import org.spockframework.mock.IMockInteraction;
import org.spockframework.mock.IMockObject;
import org.spockframework.runtime.InvalidSpecException;
import org.spockframework.util.Nullable;
import spock.lang.Specification;

import java.lang.reflect.Type;
import spock.lang.Specification;

public class MockObject implements IMockObject {
private final String name;
private final Type type;
private final Object instance;
private final Object userCreatedInstance;
private final boolean verified;
private final boolean global;
private final IDefaultResponse defaultResponse;
Expand All @@ -37,15 +39,9 @@ public class MockObject implements IMockObject {

public MockObject(@Nullable String name, Type type, Object instance, boolean verified, boolean global,
IDefaultResponse defaultResponse, Specification specification, SpecificationAttachable mockInterceptor) {
this(name, type, instance, null, verified, global, defaultResponse, specification, mockInterceptor);
}

public MockObject(@Nullable String name, Type type, Object instance, Object userCreatedInstance, boolean verified, boolean global,
IDefaultResponse defaultResponse, Specification specification, SpecificationAttachable mockInterceptor) {
this.name = name;
this.type = type;
this.instance = instance;
this.userCreatedInstance = userCreatedInstance;
this.verified = verified;
this.global = global;
this.defaultResponse = defaultResponse;
Expand Down Expand Up @@ -74,11 +70,6 @@ public Object getInstance() {
return instance;
}

@Override
public Object getUserCreatedInstance() {
return userCreatedInstance;
}

@Override
public boolean isVerified() {
return verified;
Expand Down
Expand Up @@ -14,12 +14,19 @@

package org.spockframework.util;

import org.spockframework.gentyref.GenericTypeReflector;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.spockframework.gentyref.GenericTypeReflector;

public abstract class ReflectionUtil {
/**
Expand Down Expand Up @@ -242,4 +249,29 @@ public static boolean isToStringOverridden(Class<?> valueClass) {
return false;
}
}

public static void deepCopyFields(Object source, Object target) {
if (!source.getClass().isAssignableFrom(target.getClass())) {
throw new IllegalArgumentException("source and target are not compatible.");
}
Class<?> clazz = source.getClass();
while (!clazz.equals(Object.class)) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
copyField(field, source, target);
}
clazz = clazz.getSuperclass();
}
}

private static void copyField(Field field, Object source, Object target) {
boolean accessible = field.isAccessible();
field.setAccessible(true);
try {
field.set(target, field.get(source));
} catch (IllegalAccessException e) {
// ignore
}
field.setAccessible(accessible);
}
}
Expand Up @@ -113,6 +113,18 @@ class JavaSpies extends Specification {
result == "singing, singing"
}
def "sping on concrete instances can use partial mocking"() {
def person = Spy(new Person())
when:
def result = person.work()
then:
1 * person.work()
1 * person.getTask() >> "work"
result == "work, work"
}
@Issue("https://github.com/spockframework/spock/issues/769")
def "can spy on instances of classes with no default constructor"() {
given:
Expand Down

0 comments on commit 38c58e2

Please sign in to comment.