rideronthestorm
rideronthestorm

Reputation: 747

Unpredicatble stub initialization in spock

This is a sample spock spec method (I know it doesn't make sense to test what stub is returning, this is a simplification for this question purpose only):

def "my test"() {
   given: 
   var upload = Mock(Upload){
       waitForCompletion() >> { throw new InterruptedException() }
   }

   var transferManager = Mock(TransferManager) {
       upload(_,_,_) >> upload
   }
   
   when: 
   var up = transferManager.upload(null, null, null)
   up.waitForCompletion()  
   
   then:
   thrown(InterruptedException) 
}

I assume that the test is quite straightforward and it should pass, but it gives me:

Expected exception of type 'java.lang.InterruptedException', but got 'java.lang.NullPointerException'

Basically, transferManager.upload() returns the default null instead of configured upload mock.

Now, if I change transferManager initialization to this:

   var transferManager = Mock(TransferManager)
   transferManager.upload(_,_,_) >> upload

It starts to work as expected. It seems to me that the problem exists only when stub uses another stub. For example, when using upload stub directly:

when:
upload.waitForCompletion()

it works as expected (it passes).

Also when I change transferManager initialization so that it doesn't use another stub:

var transferManager = Mock(TransferManager) {
    throw new InterruptedException()
}

the test also passes.

So my question is, why this works as expected:

   var transferManager = Mock(TransferManager)
   transferManager.upload(_,_,_) >> upload

while that doesn't set up upload method properly:

   var transferManager = Mock(TransferManager) {
       upload(_,_,_) >> upload
   }

?

Upvotes: 1

Views: 347

Answers (2)

kriegaex
kriegaex

Reputation: 67417

Good catch, I think you found a bug.

Update: My answer is plain wrong, please disregard it. Leonard is absolutely right (and I am stupid and ugly), I overlooked the naming issue.

I am assuming you use Spock 2.0 and a Java 10+ JDK, because I see var instead of def in your specification. But the result is the same with def, even on Spock 1.3 and Groovy 2.5.

I created an MCVE and Spock issue #1351 on your behalf.

Upvotes: 2

Leonard Brünings
Leonard Brünings

Reputation: 13242

This is caused because you named the variable upload which is the same name as the method you want to call on TransferManager.

var upload = Mock(Upload){
  waitForCompletion() >> { throw new InterruptedException() }
}

var transferManager = Mock(TransferManager) {
  upload(_,_,_) >> upload
}

In this case the local variable has a higher precedence over the delegation of the closure, which is why things go awry. I don't think there is much we can do on the Spock side as this how groovy works.

def upload = new Upload()
def transferManager = new TransferManager()
transferManager.with {
  upload("a", "b", "c")
}

will fail with groovy.lang.MissingMethodException: No signature of method: Upload.call() is applicable for argument types: (String, String, String) values: [a, b, c]

The easiest way to fix the example, is to rename the variable to something non-conflicting. Alternatively, you can prefix the stubbing with it. to make it unambiguous.

Upvotes: 4

Related Questions