How to mock a service with spock in micronaut when testing a rabbit consumer?

414 Views Asked by At

I'm currently working with:

  • Micronaut 3.7.3
  • RabbitMQ 3.11.2
  • Spock
  • Groovy / Java 17

I'm implementing a rabbitmq consumer for a simple demo project following the guidelines from micronaut project (https://micronaut-projects.github.io/micronaut-rabbitmq/3.1.0/guide/index.html)

I'm trying to mock a service that is a dependency of my rabbitmq consumer.

I've tried this approach that does not seem to work:

@MicronautTest
@Subject(SampleRequestConsumer)
class SampleRequestConsumerSpec extends Specification {

    @Inject
    ExternalWorkflowProducer externalWorkflowProducer

    @Inject
    SampleRequestConsumer sampleRequestConsumer

    @Inject
    SimpleService simpleService

    @MockBean(SimpleService)
    SimpleService simpleService() {
        Mock(SimpleService)
    }


    def "It receives a sampleRequest message in the simple.request queue"() {
        when:
        externalWorkflowProducer.send(new SampleRequest(message: "Request1"))

        then:
        sleep(100)

        1 * simpleService.handleSimpleRequest(_ as SampleRequest) >> { SampleRequest request ->
            assert request.message != null
        }
    }

}

I get this error when running the integration test:

Too few invocations for:

1 * simpleService.handleSimpleRequest(_ as SampleRequest) >> { SampleRequest request ->
            assert request.message != null
        }   (0 invocations)

Unmatched invocations (ordered by similarity):

None

See full source code on GitHub: https://github.com/art-dambrine/micronaut-rabbitmq-spock-mocking/blob/test-with-mq/src/test/groovy/com/company/microproject/amqp/consumer/SampleRequestConsumerSpec.groovy

Also notice that when I'm not reading from the queue and directly calling the method sampleRequestConsumer.receive([message: "Request1"]) mocking for the simpleService is working : https://github.com/art-dambrine/micronaut-rabbitmq-spock-mocking/blob/test-without-mq/src/test/groovy/com/company/microproject/amqp/consumer/SampleRequestConsumerSpec.groovy

Thanks in advance for your insight

IMPORTANT

Please use the branch test-with-mq. The branch test-without-mq's tests will succeed because it's not using rabbitMQ. This is an attempt to demonstrate that the issue lies in testing RabbitMQ consumers.

2

There are 2 best solutions below

0
On BEST ANSWER

Moving the sleep() instruction to the when: block fixed the test.

Indeed, what is behind the then: block is not executed after the externalWorkflowProducer.send(), it is executed before by Spock. This instruction:

1 * simpleService.handleSimpleRequest(_ as SampleRequest)

is creating an interaction in the scope of the feature method, and it is executed when the specification is configured.

Adding a sleep() instruction there, is not leaving the spec time for the consumer to receive the message. You need to add the sleep() after the send(). This is when your test needs to let the consumer time to execute.

Note: the closure itself:

 { SampleRequest request ->
            assert request.message != null
 }

would be executed afterwards, but only the closure. The sleep instruction was already executed when configuring the Spec. The Closure is not executed in this case, because the test finishes before the thread of the consumer can invoke the mock.

In summary:

This works:

    def "It receives a sampleRequest message in the simple.request queue"() {
        when:
        externalWorkflowProducer.send(new SampleRequest(message: "Request1"))
        sleep(100)

        then:
        1 * simpleService.handleSimpleRequest(_ as SampleRequest) >> { SampleRequest request ->
            assert request.message != null
        }
    }

And this doesn't work:

    def "It receives a sampleRequest message in the simple.request queue"() {
        when:
        externalWorkflowProducer.send(new SampleRequest(message: "Request1"))

        then:
        sleep(100)
        1 * simpleService.handleSimpleRequest(_ as SampleRequest) >> { SampleRequest request ->
            assert request.message != null
        }
    }
0
On

As @LuisMuñiz pointed out, the interactions declared in the then block are actually moved around. It creates an interaction scope that contains all the interactions, the setup of that happens immediately before the when block executes and the verification that all interactions had taken place happens before any other instruction in the then block.

That out of the way, I would advise against using any kind of sleeps for your code. At best you are just waiting uselessly, at worst you didn't wait long enough and you test breaks. It is preferable to use one or more CountDownLatch instances to synchronize your test.

    def "It receives a sampleRequest message in the simple.request queue"() {
        given:
        def latch = new CountDownLatch(1)

        when:
        externalWorkflowProducer.send(new SampleRequest(message: "Request1"))
        latch.await()

        then:
        1 * simpleService.handleSimpleRequest(_ as SampleRequest) >> { SampleRequest request ->
            assert request.message != null
            latch.countDown()
        }
    }

This way you test will wait until you mock was called, but then immediately finish.

You can also use latch.await(long timeout, TimeUnit unit) with a generous timeout, to guard against your test hanging indefinitely.