Search code examples
javaunit-testinggroovyspock

SpockFramework Global Mocks not working as expected


I'm using the following test as an example to showcase a similar issue I'm seeing. I think it's just a misunderstanding on my part about how global mocks work in SpockFramework.

  void "test"() {
    when:
    TestStage stage = new TestStage("John")
    GroovyMock(TestStep.class, global: true) {
      getName() >> "Joe"
    }
    then:
    stage.run() == "Joe"
  }

This test should create a test stage supplying a default name. But then I create a global mock of a class inside of TestStage to override the return value. IE: I'm only trying to test the functionality of TestStage not TestStep. If TestStep were to make only changes, I don't want to know about them, I'll test those separately. However when I run this test, it looks like the global mock never takes effect as the returned name is still "John", which is what I supplied originally.

stage.run() == "Joe"
|     |     |
|     John  false

Here's the two sample classes used to test this.

class TestStage {
  TestStep step

  TestStage(String name) {
    this.step = new TestStep(name)
  }

  String run() {
    return step.getName()
  }

}
class TestStep {
    private String name

    TestStep(String name) {
      this.name = name
    }

    String getName() {
      return this.name
    }
}

Solution

  • Actually, you are asking a good question here. According to the Spock manual, it seems as if you could use GroovyMock and GroovyStub in order to globally replace instances and stub their methods as you tried to do, even though if I were you I would have created the global mock object first before implicitly using it in the constructor of the object depending on it. But anyway, it does not work as expected, like you said.

    When I searched the Spock manual and the Spock source code for examples concerning GroovyMock, nowhere did I find a single one involving anything else than static methods. The test coverage there is quite bad, actually. Usually, if the manual does not help me, I look if I can infer from the tests how to use a feature. In this case, I had to try by myself.

    The first thing I noticed is the completely counter-intuitive fact that when calling a constructor on a global GroovyMock or GroovyStub, it returns null!!! This is a real caveat. In a way, constructors are treated like normal mock methods here, also returning null. Nowhere does any official source mention that, and I also think it should be changed to default to returning a normal Spock mock instead (if the class is mockable, i.e. non-final).

    Now this is also the key to the solution: You need to stub one or more constructors to return something else than null, e.g. a previously created normal instance or a Spock mock/stub/spy.

    Here is a slightly altered version of your source code. I renamed the application classes so as not to contain Test in their names. All those Test class names were a little confusing to me, especially because I also named my Spock specification *Test, as I usually do instead of *Spec, because then Maven Surefire/Failsafe can detect it automatically without extra configuration.

    I also added a static method to the class to be mocked in order to show you the syntax for stubbing that, too. That is just a free add-on and not directly related to your question.

    My test shows three variants:

    • using a classical Spock mock and injecting it into the subject under test
    • using a global GroovySpy, which is always based on a real object (or instructed to create one). Therefore, you do not need to stub a contructor.
    • using a global GroovyMock with an explicitly stubbed constructor. In my example it returns a regular Spock mock with a stubbed method, but it could also return a normal instance.
    package de.scrum_master.stackoverflow.q61667088
    
    class Step {
      private String name
    
      Step(String name) {
        this.name = name
      }
    
      String getName() {
        return this.name
      }
    
      static String staticMethod() {
        return "original"
      }
    }
    
    package de.scrum_master.stackoverflow.q61667088
    
    class Stage {
      Step step
    
      Stage(String name) {
        this.step = new Step(name)
      }
    
      String run() {
        return step.getName()
      }
    }
    
    package de.scrum_master.stackoverflow.q61667088
    
    import spock.lang.Specification
    
    class GlobalMockTest extends Specification {
    
      def "use Spock mock"() {
        given:
        def step = Mock(Step) {
          getName() >> "Joe"
        }
        def stage = new Stage("John")
        stage.step = step
    
        expect:
        stage.run() == "Joe"
      }
    
      def "use global GroovySpy"() {
        given:
        GroovySpy(Step, global: true) {
          getName() >> "Joe"
        }
        Step.staticMethod() >> "stubbed"
        def stage = new Stage("John")
    
        expect:
        Step.staticMethod() == "stubbed"
        stage.run() == "Joe"
      }
    
      def "use global GroovyMock"() {
        given:
        GroovyMock(Step, global: true)
        new Step(*_) >> Mock(Step) {
          getName() >> "Joe"
        }
        Step.staticMethod() >> "stubbed"
        def stage = new Stage("John")
    
        expect:
        Step.staticMethod() == "stubbed"
        stage.run() == "Joe"
      }
    
    }
    

    P.S.: I think, you probably read the Spock manual, but just in case: If your GroovyMock/Stub/Spy target is implemented in a language other than Groovy such as Java or Kotlin, this will not work because then Groovy* will behave like a regular Spock mock/stub/spy.


    Update: I just created Spock issue #1159, first of all in order to get this behaviour documented and covered by tests, but in order to get it also changed, if it was not intended like this.