'Assert mock call with argument assigned in where block in Spock

I have a method that among others generates an identifier, sends it to some external dependency and returns it. I want to have a unit test that tests if the same value that was sent there is returned.

Let's say that the test and the code look like this:

  def "test"() {
    given:
      def testMock = Mock(TestMock)
    and:
      def x
    when:
      def testClass = new TestClass()
      x = testClass.callMethod(testMock)
    then:
      1 * testMock.method(x)
  }

  static interface TestMock {
    void method(double x)
  }

  static class TestClass {
    double callMethod(TestMock mock) {
      double x = Math.random()
      mock.method(x)
      return x
    }
  }

The code works correct, however the test fails with a message:

One or more arguments(s) didn't match:
0: argument == expected
   |        |  |
   |        |  null
   |        false
   0.5757686318956925

So it looks like the mock check in then is done before the value is assigned in when block.

Is there a way to make Spock assign this value before he checks the mock call? Or can I do this check in a different way?

The only idea I have is to inject an id-generator to the method (or actually to the class) and stub it in the test, but it would complicate the code and I would like to avoid it.

I fixed code example according to kriegaex comment.



Solution 1:[1]

Fixing the sample code

Before we start, there are two problems with your sample code:

  • 1 * testMock(x) should be 1 * testMock.method(x)
  • callMethod should return double, not int, because otherwise the result would always be 0 (a double between 0 and 1 would always yield 0 when converted to an integer).

Please, next time make sure that your sample code not only compiles but also does the expected thing. Sample code is only helpful if it does not have extra bugs, which a person trying to help you needs to fix first before being able to focus on the actual problem later on.

The problem at hand

As you already noticed, interactions, even though lexically defined in a then: block, are transformed in such a way by Spock's compiler AST transformations, that they are registered on the mock when the mock is initialised. That is necessary because the mock must be ready before calling any methods in the when: block. Trying to directly use a result only known later while already executing the when: block, will cause the problem you described. What was first, chicken or egg? In this case, you cannot specify a method argument constraint, using the future result of another method calling the mock method in the constraint.

The workaround

A possible workaround is to stub the method and capture the argument in the closure calculating the stub result, e.g. >> { args -> arg = args[0]; return "stubbed" }. Of course, the return keyword is redundant in the last statement of a closure or method in Groovy. In your case, the method is even void, so you do not need to return anything at all in that case.

An example

I adapted your sample code, renaming classes, methods and variables to more clearly describe which is what and what is happening:

package de.scrum_master.stackoverflow.q72029050

import spock.lang.Specification

class InteractionOnCallResultTest extends Specification {
  def "dependency method is called with the expected argument"() {
    given:
    def dependency = Mock(Dependency)
    def randomNumber
    def dependencyMethodArg

    when:
    randomNumber = new UnderTest().getRandomNumber(dependency)

    then:
    1 * dependency.dependencyMethod(_) >> { args -> dependencyMethodArg = args[0] }
    dependencyMethodArg == randomNumber
  }
}
interface Dependency {
  void dependencyMethod(double x)
}
class UnderTest {
  double getRandomNumber(Dependency dependency) {
    double randomNumber = Math.random()
    dependency.dependencyMethod(randomNumber)
    return randomNumber
  }
}

Try it in the Groovy Web Console.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1