1

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 printlns 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.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • Can you come up with a small example which I can run that shows your problem? – tim_yates Feb 10 '18 at 23:33
  • @tim_yates kriegaex has managed to reproduce the issue (although I'm a bit confused because `TextField` has an inherited method `setStringValue` (not `stringValue`), but no `getStringValue`, so I'm not even sure this is really a property). If you have a moment perhaps you'd like to glimpse at my "answer" where I show the (sort of) successful use of this "global GroovyMock" technique. – mike rodent Feb 11 '18 at 19:41

2 Answers2

4

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"
  }
}
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Thanks very much. I had already stumbled on global mock, and used it to mock another Lucene `final` class, but (obviously) my Spock knowledge is rubbish. I have a **question**: to my utter astonishment just setting up this TextField mock, without any attempt to get the concrete class to use it, a test in my `then` clause passed, proving conclusively that it was being used by the concrete class. Previously I had put the line `ch.metaClass.textField = textFieldMock` in my `given` clause. Adding my successful test to my answer. – mike rodent Feb 11 '18 at 13:32
0

kriegaex provided the solution.

But, as said in my comment to his answer, I don't understand how it is that the TextField mock actually gets used by the concrete class instance ch. FWIW I put in the whole version of my (now passing) test.

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 worryingly, if I change either of these "3"s above here in the then clause to another value (e.g. 4) the test fails without outputting any test results. I just get this message from 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)

... where line 255 is the line ch.parse( fileMock ). This obviously doesn't happen with my wrapper class version... so for the moment I've gone back to that!

PPS I'm aware that I'm probably testing too many things at once here... but as a neophyte when it comes to TDD I often find one of the biggest puzzles is how to dig inside methods which do a few things with quite a few cooperating classes. The classes involved above prove somewhat inseparable (to me anyway).

mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • A global mock it truely global for the lifetime of a feature method, see the manual, section [Mocking All Instances of a Type](http://spockframework.org/spock/docs/1.1/interaction_based_testing.html#MockingAllInstancesOfAType). I do not have much experience with that feature either because having to use it is usually a sign that you need to refactor. As for why you do not need to stub a mock's method (global mock or not), this is the very reason you you mocks for. Technically speaking, a Spock mock is also a stub, see [my general answer here](https://stackoverflow.com/a/24415828/1082681). – kriegaex Feb 12 '18 at 00:59
  • BTW, Groovy mocks/spies do not work when working with non-Groovy code either, which is what I usually do because I use Spock in order to test Java application code. So thank God the temptation to use them is small for me in the first place. BTW2, I think you are testing the external tool rather than your own application. I think you should test the right thing. – kriegaex Feb 12 '18 at 01:01
  • Yes, I think I understand global mocks now... and of course I only have to use them here 1) to mock a static method 2) to mock a method where a regular mock causes an NPE (when it shouldn't). Re what I am testing: please see my closing "PPS". In fact you have to cobble together different Lucene bits and pieces ... it doesn't "do it for you". Lucene is a difficult library to mock with. But I'm also a neophyte when it comes to mocking! – mike rodent Feb 12 '18 at 15:08