Search code examples
groovyclosuresspock

Can't get Spock stubbing to accept a generated Closure


I'm writing Spock tests, and used an inline closure to stub for simple fail/pass behaviour.

    def "test timeout"() {
        given:

        2 * feignClient.poll("foo") >>
                {
                    int retries = 0;
                    if (retries < 1) {
                        retries++
                        throw newRetryable()
                    }
                    pollWaitSuccessResponseEntity
                }

So I tried to refactor the closure to a named Closure:

        def retryClosure = {
                    int retries = 0;
                    if (retries < 1) {
                        retries++
                        throw newRetryable()
                    }
                    pollWaitSuccessResponseEntity
                }
        2 * feignClient.poll("foo") >> retryClosure

The test fails with the following error:

Caused by: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object
 'com.example.service.FooServiceTest$__spock_initializeFields_closure2@3243178c' with class 'com.example..FooServiceTest$__spock_initializeFields_closure2' to class 'org.springframework.http.ResponseEntity'

Solution

  • Spock relies heavily on AST-Transformations, for that it is necessary that certain constructs are used.

    1 * service.doSomething() >> x will just return x

    1 * service.doSomething() >> { x } will run the code in the closure and return x

    So, if you want to delay the execution of the response code, but still want to put it in a variable you need to wrap the execution in a closure.

    def myClosure = {
      otherService.foo()
    }
    
    2 * service.doSomething() >> { myClosure() }
    

    Just know that you could use response chaining instead.

    2 * feignClient.poll("foo") >> { throw newRetryable() } >> pollWaitSuccessResponseEntity
    

    The main issue with your closure code, is that you are keeping the state inside the closure, and thus it will be reset on each invocation. You would need to move the retry counter out of the closure, so that the state would be kept between invocations.