Index: src/main/groovy/mock/interceptor/MockFor.groovy =================================================================== --- src/main/groovy/mock/interceptor/MockFor.groovy (revision 19142) +++ src/main/groovy/mock/interceptor/MockFor.groovy Wed Feb 03 22:38:26 EST 2010 @@ -19,7 +19,7 @@ /** * Facade over the Mocking details. - * A Mock's expectation is always sequence dependent and it's use always ends with a verify(). + * A Mock's expectation is always sequence dependent and it's use automatically ends with a verify(). * * @see StubFor * @author Dierk Koenig @@ -33,9 +33,12 @@ Map instanceExpectations = [:] Class clazz - MockFor(Class clazz) { + MockFor(Class clazz, boolean interceptConstruction=false) { + if (interceptConstruction && !GroovyObject.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("MockFor with constructor interception enabled is only allowed for Groovy objects but found: " + clazz.name) + } this.clazz = clazz - proxy = MockProxyMetaClass.make(clazz) + proxy = MockProxyMetaClass.make(clazz, interceptConstruction) demand = new Demand() expect = new StrictExpectation(demand) proxy.interceptor = new MockInterceptor(expectation: expect) @@ -59,6 +62,10 @@ demand.ignore.put(filter, filterBehavior) } + def ignore(Object filter) { + demand.ignore.put(filter, MockProxyMetaClass.FALL_THROUGH_MARKER) + } + Object proxyInstance() { proxyInstance(null) } Index: src/main/groovy/mock/interceptor/MockProxyMetaClass.java =================================================================== --- src/main/groovy/mock/interceptor/MockProxyMetaClass.java (revision 19160) +++ src/main/groovy/mock/interceptor/MockProxyMetaClass.java Thu Feb 04 00:40:49 EST 2010 @@ -28,63 +28,144 @@ public class MockProxyMetaClass extends ProxyMetaClass { + public final boolean interceptConstruction; + private boolean fallingThrough; + + static class FallThroughMarker extends Closure { + public FallThroughMarker(Object owner) { + super(owner); + } + } + static final FallThroughMarker FALL_THROUGH_MARKER = new FallThroughMarker(new Object()); + /** * @param adaptee the MetaClass to decorate with interceptability */ public MockProxyMetaClass(MetaClassRegistry registry, Class theClass, MetaClass adaptee) throws IntrospectionException { + this(registry, theClass, adaptee, false); + } + + /** + * @param adaptee the MetaClass to decorate with interceptability + */ + public MockProxyMetaClass(MetaClassRegistry registry, Class theClass, MetaClass adaptee, boolean interceptConstruction) throws IntrospectionException { super(registry, theClass, adaptee); + this.interceptConstruction = interceptConstruction; } /** * convenience factory method for the most usual case. */ public static MockProxyMetaClass make(Class theClass) throws IntrospectionException { + return make(theClass, false); + } + + /** + * convenience factory method allowing interceptConstruction to be set. + */ + public static MockProxyMetaClass make(Class theClass, boolean interceptConstruction) throws IntrospectionException { MetaClassRegistry metaRegistry = GroovySystem.getMetaClassRegistry(); MetaClass meta = metaRegistry.getMetaClass(theClass); - return new MockProxyMetaClass(metaRegistry, theClass, meta); + return new MockProxyMetaClass(metaRegistry, theClass, meta, interceptConstruction); } public Object invokeMethod(final Object object, final String methodName, final Object[] arguments) { - if (null == interceptor) { + if (null == interceptor && !fallingThrough) { throw new RuntimeException("cannot invoke method '" + methodName + "' without interceptor"); } - return interceptor.beforeInvoke(object, methodName, arguments); + Object result = FALL_THROUGH_MARKER; + if (interceptor != null) { + result = interceptor.beforeInvoke(object, methodName, arguments); - } + } + if (result == FALL_THROUGH_MARKER) { + Interceptor saved = interceptor; + interceptor = null; + boolean savedFallingThrough = fallingThrough; + fallingThrough = true; + result = super.invokeMethod(object, methodName, arguments); + fallingThrough = savedFallingThrough; + interceptor = saved; + } + return result; + } public Object invokeStaticMethod(final Object object, final String methodName, final Object[] arguments) { - if (null == interceptor) { + if (null == interceptor && !fallingThrough) { throw new RuntimeException("cannot invoke static method '" + methodName + "' without interceptor"); } - return interceptor.beforeInvoke(object, methodName, arguments); + Object result = FALL_THROUGH_MARKER; + if (interceptor != null) { + result = interceptor.beforeInvoke(object, methodName, arguments); - } + } + if (result == FALL_THROUGH_MARKER) { + Interceptor saved = interceptor; + interceptor = null; + boolean savedFallingThrough = fallingThrough; + fallingThrough = true; + result = super.invokeStaticMethod(object, methodName, arguments); + fallingThrough = savedFallingThrough; + interceptor = saved; + } + return result; + } public Object getProperty(Class aClass, Object object, String property, boolean b, boolean b1) { - if (null == interceptor) { + if (null == interceptor && !fallingThrough) { throw new RuntimeException("cannot get property '" + property + "' without interceptor"); } - if (interceptor instanceof PropertyAccessInterceptor) { - return ((PropertyAccessInterceptor) interceptor).beforeGet(object, property); - } else { - return super.getProperty(aClass, object, property, b, b); + Object result = FALL_THROUGH_MARKER; + if (interceptor != null && interceptor instanceof PropertyAccessInterceptor) { + result = ((PropertyAccessInterceptor) interceptor).beforeGet(object, property); } + if (result == FALL_THROUGH_MARKER) { + Interceptor saved = interceptor; + interceptor = null; + boolean savedFallingThrough = fallingThrough; + fallingThrough = true; + result = super.getProperty(aClass, object, property, b, b1); + fallingThrough = savedFallingThrough; + interceptor = saved; - } + } + return result; + } public void setProperty(Class aClass, Object object, String property, Object newValue, boolean b, boolean b1) { - if (null == interceptor) { + if (null == interceptor && !fallingThrough) { throw new RuntimeException("cannot set property '" + property + "' without interceptor"); } - if (interceptor instanceof PropertyAccessInterceptor) { - ((PropertyAccessInterceptor) interceptor).beforeSet(object, property, newValue); - } else { - super.setProperty(aClass, object, property, newValue, b, b); + Object result = FALL_THROUGH_MARKER; + if (interceptor != null && interceptor instanceof PropertyAccessInterceptor) { + // cheat and borrow first param for result as we don't use it anyway + Object[] resultHolder = new Object[1]; + ((PropertyAccessInterceptor) interceptor).beforeSet(resultHolder, property, newValue); + result = resultHolder[0]; } + if (result == FALL_THROUGH_MARKER) { + Interceptor saved = interceptor; + interceptor = null; + boolean savedFallingThrough = fallingThrough; + fallingThrough = true; + super.setProperty(aClass, object, property, newValue, b, b1); + fallingThrough = savedFallingThrough; + interceptor = saved; - } + } + } /** * Unlike general impl in superclass, ctors are not intercepted but relayed + * unless interceptConstruction is set. */ public Object invokeConstructor(final Object[] arguments) { + if (interceptConstruction && null == interceptor) + throw new RuntimeException("cannot invoke constructor without interceptor"); + + if (interceptConstruction) { + GroovyObject newInstance = (GroovyObject) interceptor.beforeInvoke(null, getTheClass().getSimpleName(), arguments); + newInstance.setMetaClass(this); + return newInstance; + } + return adaptee.invokeConstructor(arguments); } Index: src/test/groovy/mock/interceptor/HalfMockTest.groovy =================================================================== --- src/test/groovy/mock/interceptor/HalfMockTest.groovy Thu Feb 04 01:00:33 EST 2010 +++ src/test/groovy/mock/interceptor/HalfMockTest.groovy Thu Feb 04 01:00:33 EST 2010 @@ -0,0 +1,124 @@ +package groovy.mock.interceptor + +class HalfMockTest extends GroovyTestCase { + + void setUp() { + Baz.constructorCalls = 0 + Baz.staticExistsCalls = 0 + Baz.existsCalls = 0 + } + + void testCallsConstructorOfMockedObject() { + def mock = new MockFor(Baz) + mock.use { + def baz = new Baz() + } + assert Baz.constructorCalls == 1 + } + + void testMocksNonExistingMethods() { + def mock = new MockFor(Baz) + mock.demand.doesntExist() { 'testMocksNonExistingMethods' } + mock.use { + def baz = new Baz() + assert baz.doesntExist() == 'testMocksNonExistingMethods' + } + } + + void testCallsExistingMethodsIfIgnored() { + def mock = new MockFor(Baz) + mock.ignore('exists') + mock.use { + def baz = new Baz() + baz.exists() + } + assert Baz.existsCalls == 1 + } + + void testMocksExistingMethods() { + def mock = new MockFor(Baz) + mock.demand.exists() { 'testMocksExistingMethods' } + mock.use { + def baz = new Baz() + assert baz.exists() == 'testMocksExistingMethods' + } + assert Baz.existsCalls == 0 + } + + void testMocksNonExistingStaticMethods() { + def mock = new MockFor(Baz) + mock.demand.staticDoesntExist() { 'testMocksNonExistingStaticMethods' } + mock.use { + def baz = new Baz() + assert Baz.staticDoesntExist() == 'testMocksNonExistingStaticMethods' + } + } + + void testCallsExistingStaticMethodsIfIgnored() { + def mock = new MockFor(Baz) + mock.ignore('staticExists') + mock.use { + def baz = new Baz() + Baz.staticExists() + } + assert Baz.staticExistsCalls == 1 + } + + void testMocksNonExistingProperties() { + def mock = new MockFor(Baz) + mock.demand.setNonExistingProperty() {} + mock.demand.getNonExistingProperty() {2} + mock.use { + def baz = new Baz() + baz.nonExistingProperty = 1 + assert baz.nonExistingProperty == 2 + } + } + + void testAccessesExistingPropertiesIfIgnored() { + def mock = new MockFor(Baz) + mock.ignore(~'[sg]etExistingProperty') + mock.use { + Baz baz = new Baz() + baz.existingProperty = 1 + assert baz.existingProperty == 1 + } + } + + void testAccessesExistingInheritedPropertiesIfIgnored() { + def mock = new MockFor(Bar) + mock.ignore(~'[sg]etExistingProperty') + mock.use { + Baz bar = new Bar() + bar.existingProperty = 1 + assert bar.existingProperty == 1 + } + } + +} + +class Baz { + + static existsCalls = 0, staticExistsCalls = 0, constructorCalls = 0 + def existingProperty = 0 + + Baz() { + constructorCalls++ + } + + def exists() { + existsCalls++ + } + + def callsDoesntExist() { + doesntExist() + } + + static void staticExists() { + staticExistsCalls++ + } +} + +class Bar extends Baz { + +} Index: src/main/groovy/mock/interceptor/MockInterceptor.groovy =================================================================== --- src/main/groovy/mock/interceptor/MockInterceptor.groovy (revision 19135) +++ src/main/groovy/mock/interceptor/MockInterceptor.groovy Wed Feb 03 22:55:38 EST 2010 @@ -26,24 +26,34 @@ def expectation = null - Object beforeInvoke(Object object, String methodName, Object[] arguments) { + def beforeInvoke(Object object, String methodName, Object[] arguments) { if (!expectation) throw new IllegalStateException("Property 'expectation' must be set before use.") - return expectation.match(methodName)(*arguments) + def result = expectation.match(methodName) + if (result == MockProxyMetaClass.FALL_THROUGH_MARKER) return result + return result(*arguments) } - Object beforeGet(Object object, String property) { + def beforeGet(Object object, String property) { if (!expectation) throw new IllegalStateException("Property 'expectation' must be set before use.") String name = "get${property[0].toUpperCase()}${property[1..-1]}" - return expectation.match(name)() + def result = expectation.match(name) + if (result == MockProxyMetaClass.FALL_THROUGH_MARKER) return result + return result() } void beforeSet(Object object, String property, Object newValue) { if (!expectation) throw new IllegalStateException("Property 'expectation' must be set before use.") String name = "set${property[0].toUpperCase()}${property[1..-1]}" - expectation.match(name)(newValue) + def result = expectation.match(name) + if (result != MockProxyMetaClass.FALL_THROUGH_MARKER) { + result(newValue) + result = null - } + } + // object is never used so cheat and use it for return value + if (object instanceof Object[]) ((Object[])object)[0] = result + } - Object afterInvoke(Object object, String methodName, Object[] arguments, Object result) { + def afterInvoke(Object object, String methodName, Object[] arguments, Object result) { return null // never used } Index: src/main/groovy/mock/interceptor/StubFor.groovy =================================================================== --- src/main/groovy/mock/interceptor/StubFor.groovy (revision 19142) +++ src/main/groovy/mock/interceptor/StubFor.groovy Wed Feb 03 22:38:26 EST 2010 @@ -33,9 +33,12 @@ Map instanceExpectations = [:] Class clazz - StubFor(Class clazz) { + StubFor(Class clazz, boolean interceptConstruction=false) { + if (interceptConstruction && !GroovyObject.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("StubFor with constructor interception enabled is only allowed for Groovy objects but found: " + clazz.name) + } this.clazz = clazz - proxy = MockProxyMetaClass.make(clazz) + proxy = MockProxyMetaClass.make(clazz, interceptConstruction) demand = new Demand() expect = new LooseExpectation(demand) proxy.interceptor = new MockInterceptor(expectation: expect) @@ -45,10 +48,6 @@ proxy.use closure } - def ignore(Object filter, Closure filterBehavior) { - demand.ignore.put(filter, filterBehavior) - } - void use(GroovyObject obj, Closure closure) { proxy.use obj, closure } @@ -58,6 +57,14 @@ instanceExpectations[obj].verify() } + def ignore(Object filter, Closure filterBehavior) { + demand.ignore.put(filter, filterBehavior) + } + + def ignore(Object filter) { + demand.ignore.put(filter, MockProxyMetaClass.FALL_THROUGH_MARKER) + } + Object proxyInstance() { proxyInstance(null) }