ThomasVitale / cloud-native-spring-in-action

🍃 Code samples and projects from the book "Cloud Native Spring in Action - With Spring Boot and Kubernetes" (Manning)
https://www.manning.com/books/cloud-native-spring-in-action
Apache License 2.0
426 stars 257 forks source link

Chapter 10.4.3 Exercise Solution:Integration test using real world RabbitMQ broker with testcontainers #55

Closed ongiant closed 4 months ago

ongiant commented 11 months ago

In chapter 10.4.3, there is a note that presents one exercise for reader. It says: If you want to test the application against a specific broker (in our case, it would be for RabbitMQ), you can rely on Testcontainers, as you learned in the previous chapter. I’ll leave that up to you as an exercise.

It stuck me for a long time.The solution code provided below that can serve as a reference for others:

real rabbitMQ broker integration test

package com.polarbookshop.dispatcherservice;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class DispatcherServiceApplicationTests {

    @Container
    static RabbitMQContainer rabbitMQ = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.10-management"));

    // reference document: https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/howto.html#howto.testing.testcontainers
    @DynamicPropertySource
    static void rabbitMQProperties(DynamicPropertyRegistry registry){
        registry.add("spring.rabbitmq.host", rabbitMQ::getHost);
        registry.add("spring.rabbitmq.port", rabbitMQ::getAmqpPort);
        registry.add("spring.rabbitmq.username", rabbitMQ::getAdminUsername);
        registry.add("spring.rabbitmq.password", rabbitMQ::getAdminPassword);
    }

    /**
     * @SpringBootTest annotation will auto help you to add beans(e.g. RabbitTemplate, RabbitAdmin etc.)
     * for more details, see: https://docs.spring.io/spring-amqp/docs/current/reference/html/#spring-rabbit-test
      */

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitAdmin rabbitAdmin; // you can use AmqpAdmin class too

    @Test
    void contextLoads() {
    }

    @Test
    void packAndLabel(){
        long orderId = 121;

        Queue inputQueue = this.rabbitAdmin.declareQueue();
        assert inputQueue != null;
        Binding inputBinding = new Binding(inputQueue.getName(), Binding.DestinationType.QUEUE, "order-accepted", "dispatcher-service", null);

        Queue outputQueue = this.rabbitAdmin.declareQueue();
        assert outputQueue != null;
        Binding outputBinding = new Binding(outputQueue.getName(), Binding.DestinationType.QUEUE, "order-dispatched", "#", null);

        this.rabbitAdmin.declareBinding(inputBinding);
        this.rabbitAdmin.declareBinding(outputBinding);

        rabbitTemplate.convertAndSend("order-accepted", "dispatcher-service", new OrderAcceptedMessage(orderId));

        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
            OrderDispatchedMessage message = rabbitTemplate.receiveAndConvert(outputQueue.getName(),
                10000, new ParameterizedTypeReference(){});

            assert message != null;
            assertThat(message.orderId()).isEqualTo(orderId);
            System.out.println("------------------------------------: " + message.orderId());
        });
    }
}

then you need to add awaitility dependency and Jackson2JsonMessageConverter bean for using ParameterizedTypeReference.

testImplementation "org.awaitility:awaitility:${awaitilityVersion}"
package com.polarbookshop.dispatcherservice;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * this class is created because the ParameterizedTypeReference is needed in DispatcherServiceApplicationTests
 */

@Configuration
public class RabbitMQConfig {

    @Bean
    public Jackson2JsonMessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

Reference:

  1. https://www.youtube.com/watch?v=9jZInwFtp44&ab_channel=SivaLabs
  2. https://stackoverflow.com/questions/49816044/connect-to-message-broker-with-spring-cloud-stream-from-test
ThomasVitale commented 10 months ago

@ongiant nice solution, thanks a lot for sharing this!

ThomasVitale commented 4 months ago

Thanks again for sharing this solution, I have added a link from the repo documentation: https://github.com/ThomasVitale/cloud-native-spring-in-action/blob/main/Guides/testing-rabbitmq-with-testcontainers.md