3.3 Testing Support

3.3.1 Introduction

Writing integration for asynchronous applications is necessarily more complex than testing simpler applications. This is made more complex when abstractions such as the @RabbitListener annotations come into the picture. The question being how to verify that, after sending a message, the listener received the message as expected.

The framework itself has many unit and integration tests; some using mocks, others using integration testing with a live RabbitMQ broker. You can consult those tests for some ideas for testing scenarios.

Spring AMQP version 1.6 introduced the spring-rabbit-test jar which provides support for testing some of these more complex scenarios. It is anticipated that this project will expand over time but we need community feedback to make suggestions for features needed to help with testing. Please use JIRA or GitHub Issues to provide such feedback.

3.3.2 Mockito Answer<?> Implementations

There are currently two Answer<?> implementations to help with testing:

The first, LatchCountDownAndCallRealMethodAnswer provides an Answer&lt;Void&gt; that returns null and counts down a latch.

LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(2);
doAnswer(answer)
    .when(listener).foo(anyString(), anyString());

...

assertTrue(answer.getLatch().await(10, TimeUnit.SECONDS));

The second, LambdaAnswer&lt;T&gt; provides a mechanism to optionally call the real method and provides an opportunity to return a custom result, based on the InvocationOnMock and the result (if any).

public class Foo {

    public String foo(String foo) {
        return foo.toUpperCase();
    }

}
Foo foo = spy(new Foo());

doAnswer(new LambdaAnswer<String>(true, (i, r) -> r + r))
    .when(foo).foo(anyString());
assertEquals("FOOFOO", foo.foo("foo"));

doAnswer(new LambdaAnswer<String>(true, (i, r) -> r + i.getArguments()[0]))
    .when(foo).foo(anyString());
assertEquals("FOOfoo", foo.foo("foo"));

doAnswer(new LambdaAnswer<String>(false, (i, r) ->
    "" + i.getArguments()[0] + i.getArguments()[0])).when(foo).foo(anyString());
assertEquals("foofoo", foo.foo("foo"));

When using Java 7 or earlier:

doAnswer(new LambdaAnswer<String>(true, new ValueToReturn<String>() {
    _@Override_
    public String apply(InvocationOnMock i, String r) {
        return r + r;
    }
})).when(foo).foo(anyString());

3.3.3 @RabbitListenerTest and RabbitListenerTestHarness

Annotating one of your @Configuration classes with @RabbitListenerTest will cause the framework to replace the standard RabbitListenerAnnotationBeanPostProcessor with a subclass RabbitListenerTestHarness (it will also enable @RabbitListener detection via @EnableRabbit).

The RabbitListenerTestHarness enhances the listener in two ways - it wraps it in a Mockito Spy, enabling normal Mockito stubbing and verification operations. It can also add an Advice to the listener enabling access to the arguments, result and or exceptions thrown. You can control which (or both) of these are enabled with attributes on the @RabbitListenerTest. The latter is provided for access to lower-level data about the invocation - it also supports blocking the test thread until the async listener is called.

[Important] Important
final @RabbitListener methods cannot be spied or advised; also, only listeners with an id attribute can be spied or advised.

Let’s take a look at some examples.

Using spy:

_@Configuration_
_@RabbitListenerTest_
public class Config {

    _@Bean_
    public Listener listener() {
        return new Listener();
    }

    ...

}

public class Listener {

    _@RabbitListener(id="foo", queues="#{queue1.name}")_
    public String foo(String foo) {
        return foo.toUpperCase();
    }

    _@RabbitListener(id="bar", queues="#{queue2.name}")_
    public void foo(_@Payload_ String foo, _@Header("amqp_receivedRoutingKey")_ String rk) {
        ...
    }

}

public class MyTests {

    _@Autowired_
    private RabbitListenerTestHarness harness; ![1](./Spring AMQP_files/1.png)

    _@Test_
    public void testTwoWay() throws Exception {
        assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo"));

        Listener listener = this.harness.getSpy("foo"); ![2](./Spring AMQP_files/2.png)
        assertNotNull(listener);
        verify(listener).foo("foo");
    }

    _@Test_
    public void testOneWay() throws Exception {
        Listener listener = this.harness.getSpy("bar");
        assertNotNull(listener);

        LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(2); ![3](./Spring AMQP_files/3.png)
        doAnswer(answer).when(listener).foo(anyString(), anyString()); ![4](./Spring AMQP_files/4.png)

        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");

        assertTrue(answer.getLatch().await(10, TimeUnit.SECONDS));
        verify(listener).foo("bar", this.queue2.getName());
        verify(listener).foo("baz", this.queue2.getName());
    }

}
1 Inject the harness into the test case so we can get access to the spy.
2 Get a reference to the spy so we can verify it was invoked as expected. Since this is a send and receive operation, there is no need to suspend the test thread because it was already suspended in the RabbitTemplate waiting for the reply.
3 In this case, we’re only using a send operation so we need a latch to wait for the asynchronous call to the listener on the container thread. We use one of the Answer<?> implementations to help with that.
4 Configure the spy to invoke the Answer.

Using the capture advice:

_@Configuration_
_@ComponentScan_
_@RabbitListenerTest(spy = false, capture = true)_
public class Config {

}

_@Service_
public class Listener {

    private boolean failed;

    _@RabbitListener(id="foo", queues="#{queue1.name}")_
    public String foo(String foo) {
        return foo.toUpperCase();
    }

    _@RabbitListener(id="bar", queues="#{queue2.name}")_
    public void foo(_@Payload_ String foo, _@Header("amqp_receivedRoutingKey")_ String rk) {
        if (!failed && foo.equals("ex")) {
            failed = true;
            throw new RuntimeException(foo);
        }
        failed = false;
    }

}

public class MyTests {

    _@Autowired_
    private RabbitListenerTestHarness harness; ![1](./Spring AMQP_files/1.png)

    _@Test_
    public void testTwoWay() throws Exception {
        assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo"));

        InvocationData invocationData =
            this.harness.getNextInvocationDataFor("foo", 0, TimeUnit.SECONDS); ![2](./Spring AMQP_files/2.png)
        assertThat(invocationData.getArguments()[0], equalTo("foo"));     ![3](./Spring AMQP_files/3.png)
        assertThat((String) invocationData.getResult(), equalTo("FOO"));
    }

    _@Test_
    public void testOneWay() throws Exception {
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");
        this.rabbitTemplate.convertAndSend(this.queue2.getName(), "ex");

        InvocationData invocationData =
            this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); ![4](./Spring AMQP_files/4.png)
        Object[] args = invocationData.getArguments();
        assertThat((String) args[0], equalTo("bar"));
        assertThat((String) args[1], equalTo(queue2.getName()));

        invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS);
        args = invocationData.getArguments();
        assertThat((String) args[0], equalTo("baz"));

        invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS);
        args = invocationData.getArguments();
        assertThat((String) args[0], equalTo("ex"));
        assertEquals("ex", invocationData.getThrowable().getMessage()); ![5](./Spring AMQP_files/5.png)
    }

}
1 Inject the harness into the test case so we can get access to the spy.
2 Use harness.getNextInvocationDataFor() to retrieve the invocation data - in this case since it was a request/reply scenario there is no need to wait for any time because the test thread was suspended in the RabbitTemplate waiting for the result.
3 We can then verify that the argument and result was as expected.
4 This time we need some time to wait for the data, since it’s an async operation on the container thread and we need to suspend the test thread.
5 When the listener throws an exception, it is available in the throwable property of the invocation data.