Add testcontainers-rabbitmq sample

* update the project with requested changes
* cleaned up commented code
* `travisci` distribution update
* remove transitive dependency
* generate pom
* Fix code format for tab indents
* Add `Jackson2JsonMessageConverter` to avoid Java serialization over the network
This commit is contained in:
Daniel Frey
2020-03-16 15:53:15 -04:00
committed by Artem Bilan
parent bce03dec79
commit 427f28642b
12 changed files with 750 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
Testcontainers - RabbitMQ Sample
==================================
Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.
[Testcontainers](https://www.testcontainers.org/)
This sample demonstrates how to setup and configure the embedded RabbitMQ Docker container for use in testing Spring Integration projects that require RabbitMQ.
A simple `IntegrationFlow` is setup to establish a message is published to an `OutboundGateway` and handled by some _downstream_ process.
It expects a response to come back on some reply-to channel established by the `RabbitTemplate`.
In the real world scenario, the Topic Exchange and Queues would have already been established by the _downstream_ application.
To aid in testing, when the RabbitMQ Testcontainer comes up, the correct Topic Exchange and Queues are created and provide simple message and handling and responses.
**Note**: These tests take a bit longer run to allow time for the Docker image to spin up and tear down.
## Embedded RabbitMQ
The project dependency adds Spring Boot Autoconfiguration for test containers that automatically sets up and configures the embedded docker image
```groovy
testCompile "com.playtika.testcontainers:embedded-rabbitmq:1.42"
```
Configuration is performed in `src/test/resources/application.yml` to point the Spring Boot RabbitMQ Autoconfiguration to the properties exposed by the library
```yml
spring:
rabbitmq:
host: ${embedded.rabbitmq.host}
port: ${embedded.rabbitmq.port}
username: ${embedded.rabbitmq.user}
password: ${embedded.rabbitmq.password}
virtual-host: ${embedded.rabbitmq.vhost}
```
[testcontainers-spring-boot](https://github.com/testcontainers/testcontainers-spring-boot)
## Architecture
The `IntegrationFlow` is setup to publish a message to a RabbitMQ `TopicExchange`. A `RoutingKey` is used to direct this message to an appropriate `Queue`.
The result is sent back to the _flow_ on a separate reply-to `Queue`.
It is deliberately setup this way to model a real-world scenario we ran into. The calling application is a Spring Boot application and the downstream application is a Python ML application.
## Execute the tests
The Gradle Wrapper is provided to execute the tests.
```bash
$ ./gradlew :testcontainers-rabbitmq:test
```

View File

@@ -0,0 +1,283 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
</parent>
<groupId>org.springframework.integration.samples</groupId>
<artifactId>testcontainers-rabbitmq</artifactId>
<version>5.3.0.BUILD-SNAPSHOT</version>
<name>Testcontainers RabbitMQ Sample</name>
<description>Testcontainers RabbitMQ Sample</description>
<url>https://projects.spring.io/spring-integration</url>
<organization>
<name>SpringIO</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>abilan</id>
<name>Artem Bilan</name>
<email>abilan@pivotal.io</email>
<roles>
<role>project lead</role>
</roles>
</developer>
<developer>
<id>garyrussell</id>
<name>Gary Russell</name>
<email>grussell@pivotal.io</email>
<roles>
<role>lead emeritus</role>
</roles>
</developer>
<developer>
<id>markfisher</id>
<name>Mark Fisher</name>
<email>mfisher@pivotal.io</email>
<roles>
<role>project founder and lead emeritus</role>
</roles>
</developer>
<developer>
<id>ghillert</id>
<name>Gunnar Hillert</name>
<email>ghillert@pivotal.io</email>
</developer>
</developers>
<scm>
<connection>scm:git:scm:git:git://github.com/spring-projects/spring-integration-samples.git</connection>
<developerConnection>scm:git:scm:git:ssh://git@github.com:spring-projects/spring-integration-samples.git</developerConnection>
<url>https://github.com/spring-projects/spring-integration-samples</url>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-amqp</artifactId>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
<exclusion>
<artifactId>*</artifactId>
<groupId>org.hamcrest</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.2.4</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
<exclusion>
<artifactId>*</artifactId>
<groupId>org.hamcrest</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
<exclusion>
<artifactId>junit-vintage-engine</artifactId>
<groupId>org.junit.vintage</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.13.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.playtika.testcontainers</groupId>
<artifactId>embedded-rabbitmq</artifactId>
<version>1.42</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>jackson-module-kotlin</artifactId>
<groupId>com.fasterxml.jackson.module</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<repositories>
<repository>
<id>repo.spring.io.milestone</id>
<name>Spring Framework Maven Milestone Repository</name>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
<repository>
<id>repo.spring.io.snapshot</id>
<name>Spring Framework Maven Snapshot Repository</name>
<url>https://repo.spring.io/libs-snapshot</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR3</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.0.BUILD-SNAPSHOT</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.10.2.20200130</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.2.4.RELEASE</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-bom</artifactId>
<version>5.3.0.M3</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.testcontainersrabbitmq;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.amqp.dsl.Amqp;
import org.springframework.integration.dsl.IntegrationFlow;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
public class IntegrationConfig {
@Bean
public IntegrationFlow request(RabbitTemplate amqpTemplate) {
return f -> f
.log()
.handle(Amqp.outboundGateway(amqpTemplate)
.exchangeName("downstream")
.routingKey("downstream.request"));
}
@Bean
public MessageConverter jsonMessageConverter(ObjectMapper objectMapper) {
return new Jackson2JsonMessageConverter(objectMapper);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.testcontainersrabbitmq;
import java.util.UUID;
public class Request {
private final UUID id;
private final Integer messageId;
public Request(UUID id, Integer messageId) {
this.id = id;
this.messageId = messageId;
}
public UUID getId() {
return this.id;
}
public Integer getMessageId() {
return this.messageId;
}
@Override
public String toString() {
return "Request{" +
"id=" + this.id +
", messageId=" + this.messageId +
'}';
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.testcontainersrabbitmq;
import java.util.UUID;
public class Response {
private final UUID requestId;
private final String message;
public Response(UUID requestId, String message) {
this.requestId = requestId;
this.message = message;
}
public UUID getRequestId() {
return this.requestId;
}
public String getMessage() {
return this.message;
}
@Override
public String toString() {
return "Response{" +
"requestId=" + this.requestId +
", message='" + this.message + '\'' +
'}';
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.testcontainersrabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestcontainersRabbitmqApplication {
public static void main(String[] args) {
SpringApplication.run(TestcontainersRabbitmqApplication.class, args);
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.testcontainersrabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.test.context.SpringIntegrationTest;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@SpringIntegrationTest
@Import({ Receiver.class, IntegrationConfigTests.Config.class })
class IntegrationConfigTests {
@Autowired
@Qualifier("request.input")
private MessageChannel requestInput;
@Test
public void test() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
UUID requestId = UUID.randomUUID();
Request fakeRequest = new Request(requestId, 1);
Message<?> receive =
messagingTemplate
.sendAndReceive(requestInput,
MessageBuilder
.withPayload(fakeRequest)
.setHeader("Content-Type", "application/json")
.build()
);
assertThat(receive).isNotNull();
assertThat(receive.getPayload()).isInstanceOf(Response.class);
Response actual = (Response) receive.getPayload();
assertThat(actual.getRequestId()).isEqualTo(requestId);
assertThat(actual.getMessage()).isEqualTo("This is message 1");
}
@TestConfiguration
public static class Config {
public static final String TOPIC_EXCHANGE = "downstream";
public static final String RESULTS_QUEUE = "downstream.results";
public static final String RESULTS_ROUTING_KEY = "downstream.results.#";
@Bean
TopicExchange topicExchange() {
return ExchangeBuilder
.topicExchange(TOPIC_EXCHANGE)
.build();
}
@Bean
Queue resultsQueue() {
return QueueBuilder
.nonDurable(RESULTS_QUEUE)
.build();
}
@Bean
Binding resultsBinding(TopicExchange topicExchange, Queue resultsQueue) {
return BindingBuilder.bind(resultsQueue)
.to(topicExchange)
.with(RESULTS_ROUTING_KEY);
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.testcontainersrabbitmq;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Component;
@Component
public class Receiver {
private static final Logger log = LoggerFactory.getLogger(Receiver.class);
private static final Map<Integer, String> messages;
static {
messages = new HashMap<>();
messages.put(1, "This is message 1");
messages.put(2, "This is message 2");
messages.put(3, "This is message 3");
messages.put(4, "This is message 4");
messages.put(5, "This is message 5");
}
@PostConstruct
public void initialize() {
log.info("Receiver initialized!");
}
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "downstream.request", durable = "true"),
exchange = @Exchange(value = "downstream", ignoreDeclarationExceptions = "true", type = "topic"),
key = "downstream.request.#"
)
)
@SendTo("downstream.results")
public Response handleMessage(Request request) {
log.info("handleMessage : received message [{}]", request);
Integer messageId;
if (null != request.getMessageId()) {
messageId = request.getMessageId();
}
else {
messageId = new Random().ints(1, 5).findFirst().getAsInt();
}
return new Response(request.getId(), messages.get(messageId));
}
}

View File

@@ -0,0 +1,11 @@
logging.level:
org.springframework.integration: DEBUG
com.playtika.test: DEBUG
spring:
rabbitmq:
host: ${embedded.rabbitmq.host}
port: ${embedded.rabbitmq.port}
username: ${embedded.rabbitmq.user}
password: ${embedded.rabbitmq.password}
virtual-host: ${embedded.rabbitmq.vhost}