斯波克虫?存根上的 callRealMethod() 不会抛出预期的异常

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

摘自 class CUT 待测:

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

测试代码:

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
}

输出到stdout

babbles
bubbles
babbles 2
gubbins
bubbles 2

我试图在网上找到 link 完整的 Spock Framework Javadoc...但我找不到..."non-framework" Javadoc 是 here,但是您不会在索引中找到方法 callRealMethod

从我从源码本地生成的JavadocAPI中,确实可以找到这个方法:是org.spockframework.mock.IMockInvocation的一个方法。它说:

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

我的理解(事实上)是 Stub 应该导致此 Exception 被抛出。但它似乎不是。路过的专家有什么意见吗?

前言

这是一个有趣的问题。理论上我的答案是:

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.

谜题

实际上,在我自己使用 Spock 1.1 (Groovy 2.4) 进行的测试中,我看到了与您不同甚至更奇怪的行为:无论我使用 mock、stub 还是 spy,callRealMethod() 总是调用真正的方法。这真是一个惊喜。所以,是的,这种行为与我的预期不同。在调试时查看接口实现的源代码,我也看不到任何对模拟对象类型的检查(是否是间谍?)。真正的方法只是识别和调用。

解决方案

查看 class DynamicProxyMockInterceptorAdapter 我找到了对此行为的解释:IMockInvocation Javadoc 中提到的异常仅在尝试调用接口类型的真实方法时抛出模拟,从不用于模拟或 class 类型的对象:

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);
}

所以句子“如果这个模拟对象没有底层的真实对象,抛出一个(...)异常”在本质上是正确的,但有歧义,因为它没有解释“底层真实对象”的含义。你的假设是错误的,我的也是。我们俩都吸取了教训。

现在您什么时候会看到所描述的行为?

package de.scrum_master.Whosebug;

public interface MyInterface {
  void doSomething();
}
package de.scrum_master.Whosebug

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)
  }
}

更新: 我刚刚创建了 issue #830 请求改进 Spock 的文档。