The mocked class here is org.apache.lucene.document.TextField
. setStringValue
is void
.
My Specification looks like this...
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
the corresponding app code looks like this:
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 )
The output from the println
s is
textField == null? false
textField class java.lang.Object
... which tends to prove that the mock is happening and getClass
is being successfully replaced. If I get rid of the line textFieldMock.getClass() >> Object.class
I get this output:
textField == null? false
textField null
In both cases the fail happens on the next line:
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)
Line 114 is the setStringValue
line. Field
here is the (non-final
) superclass of TextField
.
To me it appears that something funny is happening: as though Spock were saying to itself: "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..."
Why is setStringValue
not being mocked (or "substituted" or whatever the right term is for a method...)?
later
I went and took a look at Field.java in the package in question. The relevant lines are:
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;
}
... line 307 (implicated for the NPE) turns out to be the first throw new IllegalArgumentException...
line. Quite odd. Suggesting that fieldsData
is null
(as you'd expect).
But why does Spock find itself processing this bit of code in class Field
at all? Illogical: it's mocking, Jim, but not as we know it.
PS I later tried it with a (real) ConsoleHandler
and got the same results. I have just noticed that when Spock output suggests you use a GroovyMock it says "If the code under test is written in Groovy, use a Groovy mock." This class isn't... but so far in my testing code I've used GroovyMock
for several Java classes from Java packages, including others from Lucene... without this problem...
PPS workaround I got nowhere and in the end just created a wrapper class which encapsulates the offending final
TextField
(and will sprout whatever methods are needed...).
I have had experience of struggling with Lucene classes in the past: many of these turn out to be final
or to have final
methods. Before anyone makes the point that you don't need to test packages that can already be trusted (with which I agree!), you still need to test your own use of such classes as you develop your code.
I cannot really explain why it does not work for you as expected - BTW, stubbing getClass()
is a bad idea and a bad example because it could have all kinds of side effects - but I do have a workaround for you: use a global mock.
The first feature method replicates your problematic test case, the second one shows how to solve it.
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"
}
}