【问题标题】:Spock: mocking/stubbing void method of final classSpock:最终类的模拟/存根无效方法
【发布时间】:2018-07-21 06:09:46
【问题描述】:

这里的模拟类是org.apache.lucene.document.TextFieldsetStringValuevoid

我的规范看起来像这样...

given:
    ...
    TextField textFieldMock = GroovyMock( TextField )
    // textField is a field of the ConsoleHandler class, ch is a Spy of that class
    ch.textField = textFieldMock
    // same results with or without this line: 
    textFieldMock.setStringValue( _ ) >> null
    // NB I explain about this line below:
    textFieldMock.getClass() >> Object.class

对应的应用代码如下所示:

    assert textField != null
    singleLDoc.add(textField)
    writerDocument.paragraphIterator.each{

        println( "textField == null? ${ textField == null }" )
        println( "textField ${ textField.getClass() }" )

        textField.setStringValue( it.textContent ) // NB this is line 114
        indexWriter.addDocument( singleLDoc )

printlns 的输出是

textField == null? false
textField class java.lang.Object

... 这往往证明模拟正在发生并且getClass 被成功替换。如果我去掉 textFieldMock.getClass() >> Object.class 行,我会得到这个输出:

textField == null? false
textField null

在这两种情况下,失败都发生在下一行:

java.lang.NullPointerException
at org.apache.lucene.document.Field.setStringValue(Field.java:307)
at org.spockframework.mock.runtime.GroovyMockMetaClass.doInvokeMethod(GroovyMockMetaClass.java:86)
at org.spockframework.mock.runtime.GroovyMockMetaClass.invokeMethod(GroovyMockMetaClass.java:42)
at core.ConsoleHandler.parse_closure1(ConsoleHandler.groovy:114)

第 114 行是 setStringValue 行。 Field 这里是TextField 的(非final)超类。

对我来说,似乎发生了一些有趣的事情:好像 Spock 在自言自语:“啊,这个类 TextFieldfinal,所以我将咨询它的父类,并使用方法 setStringValue从那里......我发现/决定它不是一个模拟......”

为什么setStringValue 没有被嘲笑(或“替代”或任何对方法的正确术语......)?

稍后

我去查看了相关包中的 Field.java。相关行是:

  public void setStringValue(String value) {
    if (!(fieldsData instanceof String)) {
      throw new IllegalArgumentException("cannot change value type from " + fieldsData.getClass().getSimpleName() + " to String");
    }
    if (value == null) {
      throw new IllegalArgumentException("value must not be null");
    }
    fieldsData = value;
  }

... 第 307 行(涉及 NPE)原来是第一行 throw new IllegalArgumentException...。很奇怪。建议 fieldsDatanull(如您所料)。

但是,为什么 Spock 发现自己在处理 Field 类中的这段代码呢?不合逻辑:这是在嘲弄,Jim,但不是我们所知道的那样。

PS我后来用(真实的)ConsoleHandler 进行了尝试,得到了相同的结果。我刚刚注意到,当 Spock 输出建议您使用 GroovyMock 时,它会说“如果被测代码是用 Groovy 编写的,请使用 Groovy 模拟”。这个类不是......但到目前为止,在我的测试代码中,我已经将 GroovyMock 用于 Java 包中的几个 Java 类,包括来自 Lucene 的其他类......没有这个问题......

PPS 解决方法我一无所获,最后只是创建了一个包装器类,它封装了有问题的 final TextField (并将发芽任何需要的方法......)。

我过去曾与 Lucene 类作斗争:其中许多结果是 final 或具有 final 方法。在有人指出您不需要测试已经可以信任的包之前(我同意这一点!),您仍然需要在开发代码时测试自己对此类类的使用。

【问题讨论】:

  • 你能想出一个我可以运行的小例子来说明你的问题吗?
  • @tim_yates kriegaex 设法重现了这个问题(虽然我有点困惑,因为TextField 有一个继承方法setStringValue(不是stringValue),但没有getStringValue,所以我什至不确定这是否真的是一个财产)。如果你有时间,也许你想看看我的“答案”,我展示了(某种)成功使用这种“全局 GroovyMock”技术。

标签: groovy mocking spock final


【解决方案1】:

我无法真正解释为什么它不能按预期为您工作 - 顺便说一句,存根 getClass() 是一个坏主意和一个坏例子,因为它可能会产生各种副作用 - 但我确实有一个解决方法:使用全局模拟。

第一个特征方法复制你有问题的测试用例,第二个显示如何解决它。

package de.scrum_master.stackoverflow

import org.apache.lucene.document.TextField
import spock.lang.Specification

class LuceneTest extends Specification {
  def "Lucene text field normal GroovyMock"() {

    given: "normal Groovy mock"
    TextField textField = GroovyMock() {
      stringValue() >> "abc"
    }

    when: "calling parent method"
    textField.setStringValue("test")

    then: "exception is thrown"
    thrown NullPointerException

    and: "parent method stubbing does not work"
    textField.stringValue() == null
  }

  def "Lucene text field global GroovyMock"() {

    given: "global Groovy mock"
    TextField textField = GroovyMock(global: true) {
      stringValue() >> "abc"
    }

    expect: "can call parent method"
    textField.setStringValue("test")

    and: "parent method stubbing works"
    textField.stringValue() == "abc"
  }
}

【讨论】:

  • 非常感谢。我已经偶然发现了 global mock,并用它来模拟另一个 Lucene final 类,但是(显然)我的 Spock 知识是垃圾。我有一个问题:令我非常惊讶的是,只是设置了这个 TextField 模拟,没有任何尝试让具体类使用它,我的then 子句中的测试通过了,最终证明它是被具体类使用。以前我把ch.metaClass.textField = textFieldMock 放在我的given 子句中。将我成功的测试添加到我的答案中。
【解决方案2】:

kriegaex 提供了解决方案。

但是,正如我在对他的回答的评论中所说,我不明白 TextField 模拟实际上是如何被具体类实例 ch 使用的。 FWIW 我放入了我(现在通过)测试的整个版本。

def "parse should put several documents into the index"() {
    given: 
    // NB the method we need to mock is setStringValue, which is void
    // but (again to my astonishment) if the method is not mocked test still passes
    TextField textFieldMock = GroovyMock(global: true) { 
        // setStringValue() >> "abc"
    }
    // turns out not to be needed... why not???
    // ch.textField = textFieldMock

    IndexWriter indexWriterMock = Mock( IndexWriter )
    // commenting out this line means the test fails... as I'd expect
    // i.e. because I'm "injecting" the mock to be used instead of ch's field
    ch.indexWriter = indexWriterMock
    // this line included to be able to mock static method loadDocument:
    GroovyMock( TextDocument, global: true)
    def textDocMock = Mock( TextDocument )
    TextDocument.loadDocument(_) >> textDocMock
    Paragraph paraMock1 = Mock( Paragraph )
    paraMock1.textContent >> 'para 1'
    Paragraph paraMock2 = Mock( Paragraph )
    paraMock2.textContent >> 'para 2'
    Paragraph paraMock3 = Mock( Paragraph )
    paraMock3.textContent >> 'para 3'
    textDocMock.getParagraphIterator() >> [paraMock1, paraMock2, paraMock3].listIterator()
    Document lDocMock = GroovyMock( Document )
    // commenting out this line means the test fails... as I'd expect
    // i.e. because I'm "injecting" the mock to be used instead of ch's field
    ch.singleLDoc = lDocMock

    when: 
    def fileMock = Mock( File )
    fileMock.path >> testFolder.root.path + '/dummy.odt'
    fileMock.name >> '/dummy.odt'
    ch.parse( fileMock )

    then: 
    3 * indexWriterMock.addDocument( lDocMock )
    // this is the crucial line which is now passing!
    3 * textFieldMock.setStringValue(_)

PS 令人担忧的是,如果我在 then 子句中将上述“3”中的任何一个更改为另一个值(例如 4),则测试会失败而不会输出任何测试结果。我刚从 Gradle 收到这条消息:

core.ConsoleHandlerUTs > Lucene 文本字段全局 GroovyMock 失败
ConsoleHandlerUTs.groovy:255 处的 org.spockframework.mock.TooFewInvocationsError
...
失败:构建失败并出现 例外。
* 出了什么问题:
任务 ':currentTestBunch' 执行失败。
> java.lang.NullPointerException(没有错误信息)

... 其中第 255 行是 ch.parse( fileMock ) 行。这显然不会发生在我的包装类版本中......所以目前我已经回到那个!

PPS 我知道我可能在这里同时测试了太多东西...但是作为 TDD 的新手,我经常发现最大的难题之一是如何挖掘内部 strong> 方法,它与相当多的协作类一起做一些事情。上面涉及的课程证明有些不可分割(无论如何对我来说)。

【讨论】:

  • 一个全局模拟,它在特征方法的生命周期内真正是全局的,请参阅手册,Mocking All Instances of a Type 部分。我对该功能也没有太多经验,因为必须使用它通常表明您需要重构。至于为什么你不需要存根模拟的方法(全局模拟与否),这就是你模拟的原因。从技术上讲,Spock 模拟也是一个存根,请参阅my general answer here
  • 顺便说一句,Groovy 模拟/间谍在使用非 Groovy 代码时也不起作用,这是我通常做的,因为我使用 Spock 来测试 Java 应用程序代码。所以感谢上帝,首先使用它们的诱惑对我来说很小。 BTW2,我认为您正在测试外部工具而不是您自己的应用程序。我认为你应该测试正确的东西。
  • 是的,我想我现在了解全局模拟...当然我只需要在这里使用它们 1) 模拟静态方法 2) 模拟常规模拟导致 NPE 的方法(当它不应该时)。关于我正在测试的内容:请参阅我的结束“PPS”。事实上,你必须将不同的 Lucene 零碎拼凑在一起……它不会“为你做”。 Lucene 是一个难以模拟的库。但是在嘲笑方面我也是新手!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-17
相关资源
最近更新 更多