Search code examples
spockstubspy

Spock bug? callRealMethod() on stub does not throw expected exception


Extract from class CUT under test:

def compileOutputLines( TopDocs topDocs ) {
    println "gubbins"
}

Test code:

def "my feature"(){
    given:
    CUT stubCut = Stub( CUT ){
        compileOutputLines(_) >> { TopDocs mockTD -> 
            // NB no Exception is thrown
            // try {
            println "babbles"
            callRealMethod()
            println "bubbles"
            // }catch( Exception e ) {
            //  println "exception $e"
            // }
        }
    }
    CUT spyCut = Spy( CUT ){
        compileOutputLines(_) >> { TopDocs mockTD ->
            println "babbles 2"
            callRealMethod()
            println "bubbles 2"
        }
    }

    when:
    stubCut.compileOutputLines( Mock( TopDocs ))
    spyCut.compileOutputLines( Mock( TopDocs ))

    then:
    true
}

Output to stdout:

babbles
bubbles
babbles 2
gubbins
bubbles 2

I tried to find a link online to the full Spock Framework Javadoc... but I couldn't find it... the "non-framework" Javadoc is here, but you won't find the method callRealMethod in the index.

From the Javadoc API I have generated locally from the source, I can indeed find this method: it is a method of org.spockframework.mock.IMockInvocation. It says:

java.lang.Object callRealMethod()

Delegates this method invocation to the real object underlying this mock object, including any method arguments. If this mock object has no underlying real object, a CannotInvokeRealMethodException is thrown.

Returns: the return value of the method to which this invocation was delegated

My understanding (such as it is) is that a Stub should cause this Exception to be thrown. But it doesn't appear to be. Any comment from a passing expert?


Solution

  • Preface

    This is an interesting question. In theory my answer would be:

    callRealMethod() is only available for spies, not for mocks or stubs. It is also only mentioned in the chapter about spies, did you notice?

    Think about it: A spy wraps a real object, so there you have a reference to a real method you can call. The same is not true for mocks and stubs which are just no-op subclasses. If you could call a real method for a stub, it would be a spy.

    The puzzle

    In reality I am seeing a different, even weirder behaviour from yours in my own test with Spock 1.1 (Groovy 2.4): No matter if I use a mock, stub or spy, callRealMethod() always calls the real method. This is really a surprise. So, yes, the behaviour is different from what I would have expected. Looking through the interface implementation's source code while debugging, I also cannot see any checks for the type of mock object (is it a spy or not?). The real method is just identified and called.

    The solution

    Looking at class DynamicProxyMockInterceptorAdapter I found the explanation for this behaviour: The exception mentioned in the IMockInvocation Javadoc is only thrown when trying to call the real method for an interface type mock, never for mocks or class type objects:

    public Object invoke(Object target, Method method, Object[] arguments) throws Throwable {
      IResponseGenerator realMethodInvoker = (ReflectionUtil.isDefault(method) || ReflectionUtil.isObjectMethod(method))
        ? new DefaultMethodInvoker(target, method, arguments)
        : new FailingRealMethodInvoker("Cannot invoke real method '" + method.getName() + "' on interface based mock object");
      return interceptor.intercept(target, method, arguments, realMethodInvoker);
    }
    

    So the sentence "if this mock object has no underlying real object, a (...)Exception is thrown" is in essence correct, but ambiguous because it does not explain what "underlying real object" means. Your assumption was just wrong, so was mine. Lesson learned for both of us.

    Now when would you see the described behaviour?

    package de.scrum_master.stackoverflow;
    
    public interface MyInterface {
      void doSomething();
    }
    
    package de.scrum_master.stackoverflow
    
    import org.spockframework.mock.CannotInvokeRealMethodException
    import spock.lang.Specification
    
    class MyInterfaceTest extends Specification {
      def "Try to call real method on interface mock"() {
        given:
        MyInterface myInterface = Mock() {
          doSomething() >> { callRealMethod() }
        }
        when:
        myInterface.doSomething()
        then:
        thrown(CannotInvokeRealMethodException)
      }
    
      def "Try to call real method on interface stub"() {
        given:
        MyInterface myInterface = Stub() {
          doSomething() >> { callRealMethod() }
        }
        when:
        myInterface.doSomething()
        then:
        thrown(CannotInvokeRealMethodException)
      }
    
      def "Try to call real method on interface spy"() {
        given:
        MyInterface myInterface = Spy() {
          doSomething() >> { callRealMethod() }
        }
        when:
        myInterface.doSomething()
        then:
        thrown(CannotInvokeRealMethodException)
      }
    }
    

    Update: I have just created issue #830 requesting improvements in Spock's documentation.