Spock:mocking/stubbing final class 的 void 方法
Spock: mocking/stubbing void method of final class
这里被嘲笑的class是org.apache.lucene.document.TextField
。 setStringValue
是 void
.
我的规格是这样的...
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 )
println
s 的输出是
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
)superclass。
对我来说似乎发生了一些有趣的事情:就好像 Spock 在自言自语:"ah, this class TextField
is final
, so I'll consult its parent class, and use the method setStringValue
from there... and I find/decide that it is not a mock..."
为什么 setStringValue
没有被嘲笑(或 "substituted" 或任何正确的方法术语...)?
稍后
我去查看了问题包裹中的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...
。很奇怪。建议 fieldsData
是 null
(如您所料)。
但为什么 Spock 发现自己在 class Field
中处理这段代码?不合逻辑:这是在嘲笑,吉姆,但不是我们所知道的那样。
PS 我后来用(真实的)ConsoleHandler
尝试了它并得到了相同的结果。我刚刚注意到,当 Spock 输出建议您使用 GroovyMock 时,它说 "If the code under test is written in Groovy, use a Groovy mock." This class isn't... 但到目前为止,在我的测试代码中,我已经使用了 GroovyMock
Java classes 来自 Java 包,包括来自 Lucene 的其他包...没有这个问题...
PPS 解决方法 我一无所获,最后只是创建了一个包装器 class 封装了有问题的 final
TextField
(并且会发芽任何需要的方法...)。
我过去曾有过使用 Lucene classes 的经历:其中许多结果是 final
或具有 final
方法。在任何人指出您不需要测试已经可以信任的包(我同意!)之前,您仍然需要在开发代码时测试您自己对此类 classes 的使用。
我真的无法解释为什么它不能像预期的那样为你工作 - 顺便说一句,存根 getClass()
是一个坏主意和一个坏例子,因为它可能有各种副作用 - 但我确实有适合您的解决方法:使用全局模拟。
第一个特征方法复制了有问题的测试用例,第二个特征方法展示了如何解决它。
package de.scrum_master.Whosebug
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"
}
}
kriegaex 提供了解决方案。
但是,正如我在对他的回答的评论中所说,我不明白 TextField
模拟实际上是如何被具体的 class 实例 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 text field global GroovyMock FAILED
org.spockframework.mock.TooFewInvocationsError at ConsoleHandlerUTs.groovy:255
...
FAILURE: Build failed with an
exception.
* What went wrong:
Execution failed for task ':currentTestBunch'.
> java.lang.NullPointerException (no error message)
... 其中第 255 行是行 ch.parse( fileMock )
。这显然不会发生在我的包装器 class 版本中……所以目前我已经回到那个版本了!
PPS 我知道我可能在这里一次测试了太多东西......但是作为一个新手,当谈到 TDD 时,我经常发现最大的难题之一是如何挖掘 inside 方法,这些方法通过相当多的合作 classes 来完成一些事情。上面涉及的 classes 证明有点不可分割(无论如何对我来说)。
这里被嘲笑的class是org.apache.lucene.document.TextField
。 setStringValue
是 void
.
我的规格是这样的...
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 )
println
s 的输出是
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
)superclass。
对我来说似乎发生了一些有趣的事情:就好像 Spock 在自言自语:"ah, this class TextField
is final
, so I'll consult its parent class, and use the method setStringValue
from there... and I find/decide that it is not a mock..."
为什么 setStringValue
没有被嘲笑(或 "substituted" 或任何正确的方法术语...)?
稍后
我去查看了问题包裹中的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...
。很奇怪。建议 fieldsData
是 null
(如您所料)。
但为什么 Spock 发现自己在 class Field
中处理这段代码?不合逻辑:这是在嘲笑,吉姆,但不是我们所知道的那样。
PS 我后来用(真实的)ConsoleHandler
尝试了它并得到了相同的结果。我刚刚注意到,当 Spock 输出建议您使用 GroovyMock 时,它说 "If the code under test is written in Groovy, use a Groovy mock." This class isn't... 但到目前为止,在我的测试代码中,我已经使用了 GroovyMock
Java classes 来自 Java 包,包括来自 Lucene 的其他包...没有这个问题...
PPS 解决方法 我一无所获,最后只是创建了一个包装器 class 封装了有问题的 final
TextField
(并且会发芽任何需要的方法...)。
我过去曾有过使用 Lucene classes 的经历:其中许多结果是 final
或具有 final
方法。在任何人指出您不需要测试已经可以信任的包(我同意!)之前,您仍然需要在开发代码时测试您自己对此类 classes 的使用。
我真的无法解释为什么它不能像预期的那样为你工作 - 顺便说一句,存根 getClass()
是一个坏主意和一个坏例子,因为它可能有各种副作用 - 但我确实有适合您的解决方法:使用全局模拟。
第一个特征方法复制了有问题的测试用例,第二个特征方法展示了如何解决它。
package de.scrum_master.Whosebug
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"
}
}
kriegaex 提供了解决方案。
但是,正如我在对他的回答的评论中所说,我不明白 TextField
模拟实际上是如何被具体的 class 实例 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 text field global GroovyMock FAILED
org.spockframework.mock.TooFewInvocationsError at ConsoleHandlerUTs.groovy:255
...
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':currentTestBunch'.
> java.lang.NullPointerException (no error message)
... 其中第 255 行是行 ch.parse( fileMock )
。这显然不会发生在我的包装器 class 版本中……所以目前我已经回到那个版本了!
PPS 我知道我可能在这里一次测试了太多东西......但是作为一个新手,当谈到 TDD 时,我经常发现最大的难题之一是如何挖掘 inside 方法,这些方法通过相当多的合作 classes 来完成一些事情。上面涉及的 classes 证明有点不可分割(无论如何对我来说)。