Skip to content

Commit

Permalink
Add support for spying on concrete objects #77
Browse files Browse the repository at this point in the history
  • Loading branch information
Taylor Wicksell committed Feb 6, 2017
1 parent bef969d commit 8769bee
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public interface IMockConfiguration {
* @return the interface or class type of the mock object
*/
Class<?> getType();

/**
* Returns the instance to be used as a delegate
*
* @return the instance to be used as a delegate
*/
@Nullable
Object getInstance();

/**
* Returns the exact interface or class type of the mock object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ 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
20 changes: 20 additions & 0 deletions spock-core/src/main/java/org/spockframework/mock/MockUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,24 @@ public Object createDetachedMock(@Nullable String name, Type type, MockNature na
new MockConfiguration(name, type, nature, implementation, options), classloader);
return mock;
}

/**
* Creates a detached mock.
*
* @param name the name
* @param type the type of the mock
* @param nature the nature
* @param implementation the implementation
* @param options the options
* @param classloader the classloader to use
* @return the mock
*/
@Beta
public Object createDetachedMock(@Nullable String name, Object obj, MockNature nature,
MockImplementation implementation, Map<String, Object> options, ClassLoader classloader) {

Object mock = CompositeMockFactory.INSTANCE.createDetached(
new MockConfiguration(name, obj.getClass(), obj, nature, implementation, options), classloader);
return mock;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ public CglibRealMethodInvoker(MethodProxy methodProxy) {

public Object respond(IMockInvocation invocation) {
try {
return methodProxy.invokeSuper(invocation.getMockObject().getInstance(), invocation.getArguments().toArray());
if(invocation.getMockObject().getUserCreatedInstance() != null) {
return methodProxy.invoke(invocation.getMockObject().getUserCreatedInstance(), invocation.getArguments().toArray());
} else {
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
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void run() {
}

IProxyBasedMockInterceptor mockInterceptor = new GroovyMockInterceptor(configuration, specification, newMetaClass);
return ProxyBasedMockFactory.INSTANCE.create(type, Collections.<Class<?>>singletonList(GroovyObject.class),
return ProxyBasedMockFactory.INSTANCE.create(type, configuration.getInstance(), Collections.<Class<?>>singletonList(GroovyObject.class),
configuration.getConstructorArgs(), mockInterceptor, specification.getClass().getClassLoader(),
configuration.isUseObjenesis());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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(), Collections.<Class<?>>emptyList(),
return ProxyBasedMockFactory.INSTANCE.create(configuration.getType(), configuration.getInstance(), Collections.<Class<?>>emptyList(),
configuration.getConstructorArgs(), interceptor, classLoader,
configuration.isUseObjenesis());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public JavaMockInterceptor(IMockConfiguration mockConfiguration, Specification s

public Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker) {
IMockObject mockObject = new MockObject(mockConfiguration.getName(), mockConfiguration.getExactType(),
target, mockConfiguration.isVerified(), false, mockConfiguration.getDefaultResponse(), specification, this);
target, mockConfiguration.getInstance(), mockConfiguration.isVerified(), false, mockConfiguration.getDefaultResponse(), specification, this);

if (method.getDeclaringClass() == ISpockMockObject.class) {
return mockObject;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@
import java.util.Map;

import org.spockframework.gentyref.GenericTypeReflector;
import org.spockframework.mock.IMockConfiguration;
import org.spockframework.mock.IDefaultResponse;
import org.spockframework.mock.IMockConfiguration;
import org.spockframework.mock.MockImplementation;
import org.spockframework.mock.MockNature;
import org.spockframework.util.Nullable;
import org.spockframework.util.Beta;
import org.spockframework.util.Nullable;

@Beta
public class MockConfiguration implements IMockConfiguration {
private final String name;
private final Type type;
private final Object instance;
private final MockNature nature;
private final MockImplementation implementation;
private final List<Object> constructorArgs;
Expand All @@ -38,11 +39,17 @@ public class MockConfiguration implements IMockConfiguration {
private final boolean verified;
private final boolean useObjenesis;

@SuppressWarnings("unchecked")
public MockConfiguration(@Nullable String name, Type type, MockNature nature,
MockImplementation implementation, Map<String, Object> options) {
this(name, type, null, nature, implementation, options);
}

@SuppressWarnings("unchecked")
public MockConfiguration(@Nullable String name, Type type, @Nullable Object instance, MockNature nature,
MockImplementation implementation, Map<String, Object> options) {
this.name = getOption(options, "name", String.class, name);
this.type = getOption(options, "type", Type.class, type);
this.instance = getOption(options, "instance", Object.class, instance);
this.nature = getOption(options, "nature", MockNature.class, nature);
this.implementation = getOption(options, "implementation", MockImplementation.class, implementation);
this.constructorArgs = getOption(options, "constructorArgs", List.class, null);
Expand All @@ -60,6 +67,10 @@ public String getName() {
public Class<?> getType() {
return GenericTypeReflector.erase(type);
}

public Object getInstance() {
return instance;
}

public Type getExactType() {
return type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,25 @@ 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;
private final SpecificationAttachable mockInterceptor;

private Specification specification;

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 All @@ -65,6 +72,10 @@ public Type getExactType() {
public Object getInstance() {
return instance;
}

public Object getUserCreatedInstance() {
return userCreatedInstance;
}

public boolean isVerified() {
return verified;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ public class ProxyBasedMockFactory {

public static ProxyBasedMockFactory INSTANCE = new ProxyBasedMockFactory();

public Object create(Class<?> mockType, List<Class<?>> additionalInterfaces, @Nullable List<Object> constructorArgs,
public Object create(Class<?> mockType, @Nullable Object instance, List<Class<?>> additionalInterfaces, @Nullable List<Object> constructorArgs,
IProxyBasedMockInterceptor mockInterceptor, ClassLoader classLoader, boolean useObjenesis) throws CannotCreateMockException {
Object proxy;

if (mockType.isInterface()) {
proxy = createDynamicProxyMock(mockType, additionalInterfaces, constructorArgs, mockInterceptor, classLoader);
} else if (byteBuddyAvailable && !ignoreByteBuddy) {
if(instance != null) {
throw new CannotCreateMockException(mockType, "Cannot support Spying of concrete instance using ByteBuddy. Consider setting org.spockframework.mock.ignoreByteBuddy=true");
}
proxy = ByteBuddyMockFactory.createMock(mockType, additionalInterfaces,
constructorArgs, mockInterceptor, classLoader, useObjenesis);
} else if (cglibAvailable) {
Expand Down
14 changes: 14 additions & 0 deletions spock-core/src/main/java/spock/mock/DetachedMockFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ public <T> T Stub(Map<String, Object> options, Class<T> type) {
public <T> T Spy(Class<T> type) {
return createMock(inferNameFromType(type), type, MockNature.SPY, Collections.<String, Object>emptyMap());
}

@Override
public <T> T Spy(T obj) {
return createMock(inferNameFromType(obj.getClass()), obj, MockNature.SPY, Collections.<String, Object>emptyMap());
}

/**
* Creates a spy with the specified options and type. The mock name will be the types simple name.
Expand Down Expand Up @@ -145,6 +150,15 @@ public <T> T createMock(@Nullable String name, Class<T> type, MockNature nature,
}
return (T) new MockUtil().createDetachedMock(name, type, nature, MockImplementation.JAVA, options, classLoader);
}

@SuppressWarnings("unchecked")
public <T> T createMock(@Nullable String name, T obj, MockNature nature, Map<String, Object> options) {
ClassLoader classLoader = obj.getClass().getClassLoader();
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
return (T) new MockUtil().createDetachedMock(name, obj, nature, MockImplementation.JAVA, options, classLoader);
}

private String inferNameFromType(Class<?> type) {
return type.getSimpleName();
Expand Down
17 changes: 17 additions & 0 deletions spock-core/src/main/java/spock/mock/MockFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ public interface MockFactory {
*/
@Beta
<T> T Spy(Class<T> type);

/**
* Creates a spy wrapping a provided instance.
*
* Example:
*
* <pre>
* def person = Spy(new Person()) // type is Person.class, name is "person"
* </pre>
*
* @param obj the instance to spy
* @param <T> the class type of the spy
*
* @return a spy with the specified type
*/
@Beta
<T> T Spy(T obj);

/**
* Creates a spy with the specified options and type.
Expand Down
21 changes: 21 additions & 0 deletions spock-core/src/main/java/spock/mock/MockingApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,27 @@ public <T> T Spy(Class<T> type) {
invalidMockCreation();
return null;
}

/**
* Creates a spy wrapping a provided instance.
*
* Example:
*
* <pre>
* def person = Spy(new Person()) // type is Person.class, name is "person"
* </pre>
*
* @param obj the instance to spy
* @param <T> the class type of the spy
*
* @return a spy with the specified type
*/
@Override
@Beta
public <T> T Spy(T obj) {
invalidMockCreation();
return null;
}

/**
* Creates a spy with the specified options and type. If enclosed in an variable assignment, the variable name
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.spockframework.mock

import org.spockframework.util.ReflectionUtil

import spock.lang.IgnoreIf
import spock.lang.Requires
import spock.lang.Specification
import spock.lang.Subject
import spock.mock.DetachedMockFactory
Expand Down Expand Up @@ -102,6 +106,33 @@ class DetachedMockFactorySpec extends Specification {
cleanup:
detach(spy)
}

@Requires({ReflectionUtil.isClassAvailable("net.bytebuddy.ByteBuddy") && !Boolean.getBoolean("org.spockframework.mock.ignoreByteBuddy")})
def "Spy(obj) with ByteBuddy"() {

when:
factory.Spy(new MockMe(42))

then:
CannotCreateMockException ex = thrown()
}

@IgnoreIf({ReflectionUtil.isClassAvailable("net.bytebuddy.ByteBuddy") && !Boolean.getBoolean("org.spockframework.mock.ignoreByteBuddy")})
def "Spy(obj) with CGLib" () {
given:
IMockMe spy = factory.Spy(new MockMe(42))
attach(spy)

when:
int result = spy.foo(2)

then:
result == 42
1 * spy.foo(2)

cleanup:
detach(spy)
}

private String getMockName(IMockMe mock) {
new MockUtil().asMock(mock).name
Expand All @@ -112,7 +143,9 @@ class DetachedMockFactorySpec extends Specification {
}

void detach(Object mock) {
new MockUtil().detachMock(mock)
if(mock != null) {
new MockUtil().detachMock(mock)
}
}
}

Expand Down

0 comments on commit 8769bee

Please sign in to comment.