Skip to content

Commit

Permalink
Add support for spying on concrete objects #77 (#695)
Browse files Browse the repository at this point in the history
* Add support for spying on concrete objects #77
* added spock:wrapWithSpy element to support replacement of a bean with a Spock spy
  • Loading branch information
twicksell authored and leonard84 committed Mar 22, 2017
1 parent ab6aedc commit 07185d3
Show file tree
Hide file tree
Showing 21 changed files with 333 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ public abstract class SpecInternals {
public ISpecificationContext getSpecificationContext() {
return specificationContext;
}

@Beta
public Object createMock(@Nullable String name, Type type, MockNature nature,
MockImplementation implementation, Map<String, Object> options, @Nullable Closure closure) {
return createMock(name, null, type, nature, implementation, options, closure);
}

@Beta
public Object createMock(@Nullable String name, Object instance, Type type, MockNature nature,
MockImplementation implementation, Map<String, Object> options, @Nullable Closure closure) {
Object mock = CompositeMockFactory.INSTANCE.create(
new MockConfiguration(name, type, nature, implementation, options), (Specification) this);
new MockConfiguration(name, type, instance, nature, implementation, options), (Specification) this);
if (closure != null) {
GroovyRuntimeUtil.invokeClosure(closure, mock);
}
Expand Down Expand Up @@ -174,6 +180,10 @@ Object SpyImpl(String inferredName, Class<?> inferredType, Map<String, Object> o
Object SpyImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
return createMockImpl(inferredName, inferredType, MockNature.SPY, MockImplementation.JAVA, options, specifiedType, closure);
}

Object SpyImpl(String inferredName, Class<?> inferredType, Object instance) {
return createMockImpl(inferredName, inferredType == null ? instance.getClass() : inferredType, instance, MockNature.SPY, MockImplementation.JAVA, Collections.<String, Object>emptyMap(), null, null);
}

Object GroovyMockImpl(String inferredName, Class<?> inferredType) {
return createMockImpl(inferredName, inferredType, MockNature.MOCK, MockImplementation.GROOVY, Collections.<String, Object>emptyMap(), null, null);
Expand Down Expand Up @@ -270,14 +280,19 @@ Object GroovySpyImpl(String inferredName, Class<?> inferredType, Map<String, Obj
Object GroovySpyImpl(String inferredName, Class<?> inferredType, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
return createMockImpl(inferredName, inferredType, MockNature.SPY, MockImplementation.GROOVY, options, specifiedType, closure);
}

private Object createMockImpl(String inferredName, Class<?> inferredType, MockNature nature,
MockImplementation implementation, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
return createMockImpl(inferredName, inferredType, null, nature, implementation, options, specifiedType, closure);
}

private Object createMockImpl(String inferredName, Class<?> inferredType, Object instance, MockNature nature,
MockImplementation implementation, Map<String, Object> options, Class<?> specifiedType, Closure closure) {
Type effectiveType = specifiedType != null ? specifiedType : options.containsKey("type") ? (Type) options.get("type") : inferredType;
if (effectiveType == null) {
throw new InvalidSpecException("Mock object type cannot be inferred automatically. " +
"Please specify a type explicitly (e.g. 'Mock(Person)').");
}
return createMock(inferredName, effectiveType, nature, implementation, options, closure);
return createMock(inferredName, instance, effectiveType, nature, implementation, options, closure);
}
}
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
@@ -1,8 +1,10 @@
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 @@ -18,6 +20,12 @@ 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
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 @@ -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
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,22 @@ class DetachedMockFactorySpec extends Specification {
cleanup:
detach(spy)
}

def "Spy(obj)" () {
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 +132,9 @@ class DetachedMockFactorySpec extends Specification {
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.spockframework.mock

import spock.lang.Shared;
import spock.lang.Specification;
import spock.lang.Stepwise;
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Stepwise

@Stepwise
class DetachedMockSpec extends Specification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ class JavaSpies extends Specification {
person.work() == "singing, singing"
person.work() == "singing"
}
def "can spy on concrete instances"() {
def person = Spy(new Person())
when:
def result = person.work()
then:
1 * person.work()
result == "singing, singing"
}
def "cannot spy on final classes"() {
when:
Expand Down
Loading

0 comments on commit 07185d3

Please sign in to comment.