26

How to mock spring rabbitmq/amqp so it will not fail during a Spring Boot Test while trying to auto create exchanges/queues?

Given I have a simple RabbitListener that will cause the queue and exchange to be auto created like this:

@Component
@RabbitListener(bindings = {
        @QueueBinding(
                value = @Queue(value = "myqueue", autoDelete = "true"), 
                exchange = @Exchange(value = "myexchange", autoDelete = "true", type = "direct"), 
                key = "mykey")}
)
@RabbitListenerCondition
public class EventHandler {
    @RabbitHandler
    public void onEvent(Event event) {
      ...
    }   
}

During a simple Spring Boot Test, like this:

@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = { Application.class })

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void test() {
        assertNotNull(applicationContext);
    }

}

it will fail with:

16:22:16.527 [SimpleAsyncTaskExecutor-1] ERROR o.s.a.r.l.SimpleMessageListenerContainer - Failed to check/redeclare auto-delete queue(s).
org.springframework.amqp.AmqpConnectException: java.net.ConnectException: Connection refused
    at org.springframework.amqp.rabbit.support.RabbitExceptionTranslator.convertRabbitAccessException(RabbitExceptionTranslator.java:62)
    at org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.createBareConnection(AbstractConnectionFactory.java:309)

In in this test I don't care about Rabbit/AMQP, so how can I mock the whole Rabbit/AMQP away?

domi
  • 2,167
  • 1
  • 28
  • 45

8 Answers8

18

I know this is an old topic, but I'd like to introduce a mocking library I'm developping : rabbitmq-mock.

The purpose of this mock is to mimic RabbitMQ behavior without IO (without starting a server, listening to some port, etc.) and with minor (~ none) startup time.

It is available in Maven Central:

<dependency>
    <groupId>com.github.fridujo</groupId>
    <artifactId>rabbitmq-mock</artifactId>
    <version>1.1.1</version>
    <scope>test</scope>
</dependency>

Basic use will be to override Spring configuration with a test one :

@Configuration
@Import(AmqpApplication.class)
class AmqpApplicationTestConfiguration {

    @Bean
    public ConnectionFactory connectionFactory() {
        return new CachingConnectionFactory(MockConnectionFactoryFactory.build());
    }
}

For automatic mocking of Spring beans for tests, give a look at another project I'm working on: spring-automocker

Hope this can help !

Loïc Le Doyen
  • 975
  • 7
  • 16
  • I tried this and must be doing something wrong. I'm getting: java.lang.ClassNotFoundException: com.rabbitmq.client.QueueingConsumer – disco.dan.silver Sep 20 '18 at 17:52
  • The **rabbitmq-mock** code does not use this `com.rabbitmq.client.QueueingConsumer`, what version of **amqp-client** are you using ? – Loïc Le Doyen Sep 21 '18 at 10:46
  • 4.2.0. Another dev implemented it about a year ago. I'm just trying to get the tests to run without an exception throwing. – disco.dan.silver Sep 21 '18 at 15:05
  • @disco.dan.silver you must have something wrong going with your dependency management, because I only can find **spring-rabbit (1.17.10.RELEASE)** pointing to **amqp-client (4.0.3)** and **spring-rabbit (2.0.0.RELEASE)** pointing to **amqp-client (5.0.0)**, no references to the _4.2.0 version_. Besides that, the error you linked is due to the missing _deprecated_ class `com.rabbitmq.client.QueueingConsumer`, which is present in **4.2.0**, but removed after **5.0.0**. Can you check your classpath for unwanted version of the **ampq-client** dependency ? – Loïc Le Doyen Sep 23 '18 at 10:39
  • The only thing I could find that has amqp-client is org.springframework.boot:spring-boot-starter-amqp. – disco.dan.silver Sep 24 '18 at 21:48
  • Maybe it would be easier to reason about your issue with detailed information (pom.xml or build.gradle), complete stacktrace, etc., in a separated question. In the meantime, you could check sample of use of rabbitmq-mock with spring-boot in integration tests: https://github.com/fridujo/rabbitmq-mock/tree/master/src/it/spring_boot – Loïc Le Doyen Sep 25 '18 at 07:32
  • How can I configure the spring cloud stream to consume from the mock connection? – FabianoLothor Nov 19 '19 at 14:04
9

It's not particularly easy, we generally use a JUnit @Rule to skip the test if the broker's not available.

However, we do have a lot of tests that use mocks, but you really have to understand a lot of the Spring AMQP internals to use them. You can explore the test cases in the project itself.

At one point I did attempt writing a mock broker but it ended up being too much work.

Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • 1
    strange enough, it used to work with 1.5.6 - I only had to configure a Bean like this: Mockito.mock(AmqpTemplate.class) - but now with 1.6.1 this does not work anymore :( – domi Aug 03 '16 at 15:44
  • If I'm correct, this also means that I will not be able to use any SpringBoot Test cases with a full container when ever I have such a configuration :( – domi Aug 03 '16 at 15:47
  • 1
    Mocking the template is easy enough; it's mocking broker responses (confirms, returns, deliveries) that's a lot more involved. Explain "does not work anymore" - `AmqpTemplate` is a simple `interface`; there's nothing in 1.6 that would have changed the ability to mock it. For `@RabbitListener` you would have to mock a listener container. – Gary Russell Aug 03 '16 at 16:04
  • I don't need to anything related to rabbit/amqp to work in this testcase, all I want is that the startup does not fail with the above shown error. – domi Aug 03 '16 at 19:38
  • with 1.5.6 I only had to mock the `AmqpTemplate` and then I had the embedded tomcat to startup without any error. – domi Aug 03 '16 at 19:40
  • But the `@RabbitListener` will try to connect, unless you set the autoStartup to false on the listener container factory - that would be the same on 1.5.x too; so something else is different. – Gary Russell Aug 03 '16 at 20:10
  • Thanks - I did not know about the `autoStartup`, that helps a lot! but yeah, it seems like there must be something else too... – domi Aug 04 '16 at 06:40
  • Just the name of the property that prevents auto startup: spring.rabbitmq.listener.simple.auto-startup = false – leaqui Apr 06 '21 at 14:43
9

Not sure if this is helpful but, I was having the same problem. So, I just used @MockBean on RabbitAdmin with a different profile, and did not get the same connection issues. Tests Passed.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@RunWith(SpringRunner.class)
@ActiveProfiles("my-test")
public class ServiceTests {

@Autowired
private DummyService unitUnderTest;

@MockBean
private RabbitAdmin rabbitAdmin;

// lots of tests which do not need Spring to Create a RabbitAdmin Bean
}
Rajkishan Swami
  • 3,569
  • 10
  • 48
  • 68
5

In our project, we initialize a RabbitMQ instance using a docker container locally. To run an integration test, we do spin up a RabbitMQ instance at the beginning of the test case and shut it down during clean-up.

We're using TestContainers to do just that. Please see https://www.testcontainers.org/usage/dockerfile.html and/or https://www.testcontainers.org/usage/docker_compose.html.

Sridhar
  • 1,832
  • 3
  • 23
  • 44
joy
  • 43
  • 1
  • 2
4

Somewhat similar to Rajkishan's answer that didn't work for me:

This is what worked for me instead:

@SpringBootApplication
public class MyTestsApp {
    @Bean
    @Primary
    public CachingConnectionFactory rabbitAdmin() {
        return Mockito.mock(CachingConnectionFactory.class);
    }
}

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MyTestsApp.class})
@ActiveProfiles(profiles = "test")
public class MyTests {

}
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
2

I was having a similar requirement at some point, and looked into QPid, which provides an in memory AMQP broker. It's forcing you to stay at the AMQP level, and use as little rabbitMq specific code as possible.

But I actually found another way : by tweaking the names of queues and exchanges when running tests + the auto-delete value, we're not having the issue anymore. All queue/exchange names in tests are suffixed with the user name (of the account running the tests), meaning that everyone can run the tests on their machine without impacting others.

Even in our CI pipeline, several projects may use the same exchanges/queues : we configure the values in tests to be project specific, so that even if 2 projects run their tests at the same time on the same machine with the same user, messages will not "leak" outside of current test.

This ends up being much simpler to manage than mocking or spawning an in memory broker.

Vincent F
  • 6,523
  • 7
  • 37
  • 79
1

First, create a @Configuration with ConnectionFactory in your test package:

@Configuration
public class RabbitMqConfiguration {

    @Bean
    ConnectionFactory connectionFactory() {
        return new CachingConnectionFactory();
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
       return new RabbitTemplate(connectionFactory);
    }

}

After that, set this property in your application.yml from test package:

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration

This should work for Spring Boot 2.2.x.

For Spring Boot 1.5.x I also needed to add one more dependency:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-test-support</artifactId>
    <scope>test</scope>
</dependency>

I don't know why, but without the spring-cloud-stream-test-support dependency my Integration Test try to connect on RabbitMQ broker. Even not affecting the result of the test itself, this stole a lot of seconds in each test. I already see this strange behavior in another post.

Dherik
  • 17,757
  • 11
  • 115
  • 164
0

You can also specify the profiles you want the listener be enabled. For example, we can set the test profile for our tests

@SpringBootTest
@ActiveProfiles("test")
class SomeTests {
}

and disable that profile for our Rabbit listener

@Service
@Profile("!test")
public class MyMessagesConsumer {

  @RabbitListener
  public void listenToMessage(String msg) {
     // ...
  }
}
Ariel Kogan
  • 13,791
  • 1
  • 14
  • 6