Search code examples
spock

Spock testing: Too many invocation


I wrote service for manual requeuing events from one queue to another.

public class ReQueueService {
  private final RabbitTemplate rabbitTemplate;

  public void retry() {
    InfoLog infoLog;
    while (rabbitTemplate != null && 
      (infoLog = (InfoLog) rabbitTemplate.receiveAndConvert(EVENT_WAITING_FOR_REQUEUE)) != null
    ) {
      rabbitTemplate.convertAndSend(SOME_QUEUE, infoLog.getSomeEvent());
    }
  }
}

The problem I am facing is getting:

Too many invocations for:

1 * rabbitTemplate.convertAndSend(SOME_QUEUE, _ as SomeEvent) >> {
      arguments ->
        assert infoLog.getSomeEvent() == arguments[1]
    }   (2 invocations)

Matching invocations (ordered by last occurrence):

2 * rabbitTemplate.convertAndSend(SOME_QUEUE, ...

while my code in test looks like this:

class ReQueueServiceTest extends Specification {
  def "should resend single event to some queue" () {
    given:
    InfoLog infoLog = Fixtures.createInfoLog()
    def rabbitTemplate = Mock(RabbitTemplate){
      receiveAndConvert(EVENT_WAITING_FOR_REQUEUE) >> { infoLog }
    }
    ReQueueService reSyncService = new ReQueueService(rabbitTemplate)

    when:
    reSyncService.retry()

    then:
    1 * rabbitTemplate.convertAndSend(SOME_QUEUE, _ as SomeEvent) >> {
      arguments ->
        assert infoLog.getSomeEvent() == arguments[1]
    }
  }
}

The question is why I have 2 invocations, if I stubb only one event?

EDIT:

link to repo with example: https://gitlab.com/bartekwichowski/spock-too-many


Solution

  • Thanks for the repo link. As soon as I could run the test and inspect the behaviour live, it was pretty easy to find out what was wrong. First I will make an educated guess about what you actually want to test:

    1. The mock's receiveAndConvert method should return str when it is called first and then null when called again.
    2. Subsequently you want to verify that the while loop runs exactly 1 iteration, i.e. that convertAndSend is called with exactly the parameters you expect.

    This can be achieved by

    1. receiveAndConvert("FOO") >>> [str, null]
    2. 1 * rabbitTemplate.convertAndSend("BAR", str) (no need for ugly assertions inside a stubbed method, the parameters are verified against your parameter constraints already)

    If I refactor your specification a little bit for prettier variable names and less verbosity, it looks like this:

    class ReSyncServiceTest extends Specification {
      def "should resend single event to resource sync queue"() {
        given:
        def message = "someValue"
        def rabbitTemplate = Mock(RabbitTemplate) {
          receiveAndConvert("FOO") >>> [message, null]
        }
    
        when:
        new ReSyncService(rabbitTemplate).retry()
    
        then:
        1 * rabbitTemplate.convertAndSend("BAR", message)
      }
    }
    

    P.S.: Your version with the assertion inside does not return anything explicitly, but implicitly the result of the last assertion. Be careful with that. With >> { ... } you are stubbing the method result! It would always return true in the version you have in Git and the test only terminates because you added the 1 * limit. If it was not there, you would have an endless loop. Your code did not do what you thought it did. Maybe the Spock manual can help you there. :-)


    P.P.S.: Maybe you want to refactor your application code to be a bit easier to understand and maintain and to be a little less "smart". Also there is no need to check that rabbitTemplate != null in every iteration, once should be enough. How about this?

    @Slf4j
    @Service
    @AllArgsConstructor
    public class ReSyncService {
      private final RabbitTemplate rabbitTemplate;
    
      public void retry() {
        if (rabbitTemplate == null)
          return;
        String event;
        while (null != (event = getEventFromQueue()))
          rabbitTemplate.convertAndSend("BAR", event);
      }
    
      protected String getEventFromQueue() {
        return (String) rabbitTemplate.receiveAndConvert("FOO");
      }
    }