This version is still in development and is not considered stable yet. For the latest stable version, please use Spring AMQP 3.2.5!spring-doc.cn

Testing Support

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 is how to verify that, after sending a message, the listener received the message as expected.spring-doc.cn

The framework itself has many unit and integration tests. Some using mocks while, others use integration testing with a live RabbitMQ broker. You can consult those tests for some ideas for testing scenarios.spring-doc.cn

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 the features needed to help with testing. Please use JIRA or GitHub Issues to provide such feedback.spring-doc.cn

@SpringRabbitTest

Use this annotation to add infrastructure beans to the Spring test ApplicationContext. This is not necessary when using, for example @SpringBootTest since Spring Boot’s auto-configuration will add the beans.spring-doc.cn

Beans that are registered are:spring-doc.cn

  • CachingConnectionFactory (autoConnectionFactory). If @RabbitEnabled is present, its connection factory is used.spring-doc.cn

  • RabbitTemplate (autoRabbitTemplate)spring-doc.cn

  • RabbitAdmin (autoRabbitAdmin)spring-doc.cn

  • RabbitListenerContainerFactory (autoContainerFactory)spring-doc.cn

In addition, the beans associated with @EnableRabbit (to support @RabbitListener) are added.spring-doc.cn

Junit5 example
@SpringJUnitConfig
@SpringRabbitTest
public class MyRabbitTests {

	@Autowired
	private RabbitTemplate template;

	@Autowired
	private RabbitAdmin admin;

	@Autowired
	private RabbitListenerEndpointRegistry registry;

	@Test
	void test() {
        ...
	}

	@Configuration
	public static class Config {

        ...

	}

}

Mockito Answer<?> Implementations

There are currently two Answer<?> implementations to help with testing.spring-doc.cn

The first, LatchCountDownAndCallRealMethodAnswer, provides an Answer<Void> that returns null and counts down a latch. The following example shows how to use LatchCountDownAndCallRealMethodAnswer:spring-doc.cn

LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("myListener", 2);
doAnswer(answer)
    .when(listener).foo(anyString(), anyString());

...

assertThat(answer.await(10)).isTrue();

The second, LambdaAnswer<T> 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).spring-doc.cn

Consider the following POJO:spring-doc.cn

public class Thing {

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

}

The following class tests the Thing POJO:spring-doc.cn

Thing thing = spy(new Thing());

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

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

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

Starting with version 2.2.3, the answers capture any exceptions thrown by the method under test. Use answer.getExceptions() to get a reference to them.spring-doc.cn

When used in conjunction with the @RabbitListenerTest and RabbitListenerTestHarness use harness.getLambdaAnswerFor("listenerId", true, …​) to get a properly constructed answer for the listener.spring-doc.cn

@RabbitListenerTest and RabbitListenerTestHarness

Annotating one of your @Configuration classes with @RabbitListenerTest causes the framework to replace the standard RabbitListenerAnnotationBeanPostProcessor with a subclass called RabbitListenerTestHarness (it also enables @RabbitListener detection through @EnableRabbit).spring-doc.cn

The RabbitListenerTestHarness enhances the listener in two ways. First, it wraps the listener 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 any exceptions that are 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.spring-doc.cn

final @RabbitListener methods cannot be spied or advised. Also, only listeners with an id attribute can be spied or advised.

Consider some examples.spring-doc.cn

The following example uses spy:spring-doc.cn

@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) {
        ...
    }

}

@SpringJUnitConfig
public class MyTests {

    @Autowired
    private RabbitListenerTestHarness harness; (1)

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

        Listener listener = this.harness.getSpy("foo"); (2)
        assertNotNull(listener);
        verify(listener).foo("foo");
    }

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

        LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("bar", 2); (3)
        doAnswer(answer).when(listener).foo(anyString(), anyString()); (4)

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

        assertTrue(answer.await(10));
        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. IMPORTANT: Due to the way the listener is spied, it is important to use harness.getLatchAnswerFor() to get a properly configured answer for the spy.
4 Configure the spy to invoke the Answer.

The following example uses the capture advice:spring-doc.cn

@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;
    }

}

@SpringJUnitConfig
public class MyTests {

    @Autowired
    private RabbitListenerTestHarness harness; (1)

    @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)
        assertThat(invocationData.getArguments()[0], equalTo("foo"));     (3)
        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)
        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)
    }

}
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.
When using custom Answer<?> s with the harness, in order to operate properly, such answers should subclass ForwardsInvocation and get the actual listener (not the spy) from the harness (getDelegate("myListener")) and call super.answer(invocation). See the provided Mockito Answer<?> Implementations source code for examples.

Using TestRabbitTemplate

The TestRabbitTemplate is provided to perform some basic integration testing without the need for a broker. When you add it as a @Bean in your test case, it discovers all the listener containers in the context, whether declared as @Bean or <bean/> or using the @RabbitListener annotation. It currently only supports routing by queue name. The template extracts the message listener from the container and invokes it directly on the test thread. Request-reply messaging (sendAndReceive methods) is supported for listeners that return replies.spring-doc.cn

The following test case uses the template:spring-doc.cn

@SpringJUnitConfig
public class TestRabbitTemplateTests {

    @Autowired
    private TestRabbitTemplate template;

    @Autowired
    private Config config;

    @Test
    public void testSimpleSends() {
        this.template.convertAndSend("foo", "hello1");
        assertThat(this.config.fooIn, equalTo("foo:hello1"));
        this.template.convertAndSend("bar", "hello2");
        assertThat(this.config.barIn, equalTo("bar:hello2"));
        assertThat(this.config.smlc1In, equalTo("smlc1:"));
        this.template.convertAndSend("foo", "hello3");
        assertThat(this.config.fooIn, equalTo("foo:hello1"));
        this.template.convertAndSend("bar", "hello4");
        assertThat(this.config.barIn, equalTo("bar:hello2"));
        assertThat(this.config.smlc1In, equalTo("smlc1:hello3hello4"));

        this.template.setBroadcast(true);
        this.template.convertAndSend("foo", "hello5");
        assertThat(this.config.fooIn, equalTo("foo:hello1foo:hello5"));
        this.template.convertAndSend("bar", "hello6");
        assertThat(this.config.barIn, equalTo("bar:hello2bar:hello6"));
        assertThat(this.config.smlc1In, equalTo("smlc1:hello3hello4hello5hello6"));
    }

    @Test
    public void testSendAndReceive() {
        assertThat(this.template.convertSendAndReceive("baz", "hello"), equalTo("baz:hello"));
    }

}
@Configuration
@EnableRabbit
public static class Config {

    public String fooIn = "";

    public String barIn = "";

    public String smlc1In = "smlc1:";

    @Bean
    public TestRabbitTemplate template() throws IOException {
        return new TestRabbitTemplate(connectionFactory());
    }

    @Bean
    public ConnectionFactory connectionFactory() throws IOException {
        ConnectionFactory factory = mock(ConnectionFactory.class);
        Connection connection = mock(Connection.class);
        Channel channel = mock(Channel.class);
        willReturn(connection).given(factory).createConnection();
        willReturn(channel).given(connection).createChannel(anyBoolean());
        given(channel.isOpen()).willReturn(true);
        return factory;
    }

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() throws IOException {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        return factory;
    }

    @RabbitListener(queues = "foo")
    public void foo(String in) {
        this.fooIn += "foo:" + in;
    }

    @RabbitListener(queues = "bar")
    public void bar(String in) {
        this.barIn += "bar:" + in;
    }

    @RabbitListener(queues = "baz")
    public String baz(String in) {
        return "baz:" + in;
    }

    @Bean
    public SimpleMessageListenerContainer smlc1() throws IOException {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
        container.setQueueNames("foo", "bar");
        container.setMessageListener(new MessageListenerAdapter(new Object() {

            public void handleMessage(String in) {
                smlc1In += in;
            }

        }));
        return container;
    }

}

JUnit5 Conditions

Version 2.0.2 introduced support for JUnit5.spring-doc.cn

Using the @RabbitAvailable Annotation

The custom JUnit 5 @RabbitAvailable annotation is processed by the RabbitAvailableCondition.spring-doc.cn

The annotation has three properties:spring-doc.cn

  • queues: An array of queues that are declared (and purged) before each test and deleted when all tests are complete.spring-doc.cn

  • management: Set this to true if your tests also require the management plugin installed on the broker.spring-doc.cn

  • purgeAfterEach: (Since version 2.2) when true (default), the queues will be purged between tests.spring-doc.cn

It is used to check whether the broker is available and skip the tests if not.spring-doc.cn

There are times when you want tests to fail if there is no broker, such as a nightly CI build. To disable the BrokerRunningSupport at runtime, set an environment variable called RABBITMQ_SERVER_REQUIRED to true.spring-doc.cn

You can override the broker properties, such as hostname with either setters or environment variables.spring-doc.cn

The following example shows how to override properties with setters:spring-doc.cn

@RabbitAvailable
...

@BeforeAll
static void setup() {
    RabbitAvailableCondition.getBrokerRunning().setHostName("10.0.0.1");
}

@AfterAll
static void tearDown() {
    RabbitAvailableCondition.getBrokerRunning().removeTestQueues("some.other.queue.too");
}

You can also override properties by setting the following environment variables:spring-doc.cn

public static final String BROKER_ADMIN_URI = "RABBITMQ_TEST_ADMIN_URI";
public static final String BROKER_HOSTNAME = "RABBITMQ_TEST_HOSTNAME";
public static final String BROKER_PORT = "RABBITMQ_TEST_PORT";
public static final String BROKER_USER = "RABBITMQ_TEST_USER";
public static final String BROKER_PW = "RABBITMQ_TEST_PASSWORD";
public static final String BROKER_ADMIN_USER = "RABBITMQ_TEST_ADMIN_USER";
public static final String BROKER_ADMIN_PW = "RABBITMQ_TEST_ADMIN_PASSWORD";

These environment variables override the default settings (localhost:5672 for amqp and localhost:15672/api/ for the management REST API).spring-doc.cn

Changing the host name affects both the amqp and management REST API connection (unless the admin uri is explicitly set).spring-doc.cn

BrokerRunningSupport also provides a static method called setEnvironmentVariableOverrides that lets you can pass in a map containing these variables. They override system environment variables. This might be useful if you wish to use different configuration for tests in multiple test suites. IMPORTANT: The method must be called before invoking any of the isRunning() static methods that create the rule instance. Variable values are applied to all instances created after this invocation. Invoke clearEnvironmentVariableOverrides() to reset the rule to use defaults (including any actual environment variables).spring-doc.cn

In your test cases, you can use the RabbitAvailableCondition.getBrokerRunning() when creating the connection factory; getConnectionFactory() returns the rule’s RabbitMQ ConnectionFactory. The following example shows how to do so:spring-doc.cn

@Bean
public CachingConnectionFactory rabbitConnectionFactory() {
    return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
}

In addition, the RabbitAvailableCondition supports argument resolution for parameterized test constructors and methods. Two argument types are supported:spring-doc.cn

  • BrokerRunningSupport: the instancespring-doc.cn

  • ConnectionFactory: the BrokerRunningSupport instance’s RabbitMQ connection factoryspring-doc.cn

The following example shows both:spring-doc.cn

@RabbitAvailable(queues = "rabbitAvailableTests.queue")
public class RabbitAvailableCTORInjectionTests {

    private final ConnectionFactory connectionFactory;

    public RabbitAvailableCTORInjectionTests(BrokerRunningSupport brokerRunning) {
        this.connectionFactory = brokerRunning.getConnectionFactory();
    }

    @Test
    public void test(ConnectionFactory cf) throws Exception {
        assertSame(cf, this.connectionFactory);
        Connection conn = this.connectionFactory.newConnection();
        Channel channel = conn.createChannel();
        DeclareOk declareOk = channel.queueDeclarePassive("rabbitAvailableTests.queue");
        assertEquals(0, declareOk.getConsumerCount());
        channel.close();
        conn.close();
    }

}

The preceding test is in the framework itself and verifies the argument injection and that the condition created the queue properly.spring-doc.cn

A practical user test might be as follows:spring-doc.cn

@RabbitAvailable(queues = "rabbitAvailableTests.queue")
public class RabbitAvailableCTORInjectionTests {

    private final CachingConnectionFactory connectionFactory;

    public RabbitAvailableCTORInjectionTests(BrokerRunningSupport brokerRunning) {
        this.connectionFactory =
            new CachingConnectionFactory(brokerRunning.getConnectionFactory());
    }

    @Test
    public void test() throws Exception {
        RabbitTemplate template = new RabbitTemplate(this.connectionFactory);
        ...
    }
}

When you use a Spring annotation application context within a test class, you can get a reference to the condition’s connection factory through a static method called RabbitAvailableCondition.getBrokerRunning().spring-doc.cn

The following test comes from the framework and demonstrates the usage:spring-doc.cn

@RabbitAvailable(queues = {
        RabbitTemplateMPPIntegrationTests.QUEUE,
        RabbitTemplateMPPIntegrationTests.REPLIES })
@SpringJUnitConfig
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class RabbitTemplateMPPIntegrationTests {

    public static final String QUEUE = "mpp.tests";

    public static final String REPLIES = "mpp.tests.replies";

    @Autowired
    private RabbitTemplate template;

    @Autowired
    private Config config;

    @Test
    public void test() {

        ...

    }

    @Configuration
    @EnableRabbit
    public static class Config {

        @Bean
        public CachingConnectionFactory cf() {
            return new CachingConnectionFactory(RabbitAvailableCondition
                    .getBrokerRunning()
                    .getConnectionFactory());
        }

        @Bean
        public RabbitTemplate template() {

            ...

        }

        @Bean
        public SimpleRabbitListenerContainerFactory
                            rabbitListenerContainerFactory() {

            ...

        }

        @RabbitListener(queues = QUEUE)
        public byte[] foo(byte[] in) {
            return in;
        }

    }

}

Using the @LongRunning Annotation

The @LongRunning annotation causes tests to be skipped unless an environment variable (or system property) is set to true. The following example shows how to use it:spring-doc.cn

@RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE)
@LongRunning
public class SimpleMessageListenerContainerLongTests {

    public static final String QUEUE = "SimpleMessageListenerContainerLongTests.queue";

...

}

By default, the variable is RUN_LONG_INTEGRATION_TESTS, but you can specify the variable name in the annotation’s value attribute.spring-doc.cn