使用 Testcontainers 进行 Spring Boot 应用程序的测试
由于我目前正在参与的项目可能需要使用MySQL、Kafka、Cloud Pub/Sub、AWS SQS/SNS等工具来创建应用程序,所以我考虑使用Testcontainers来测试Spring Boot应用程序。我已经制作了一个非常简单的演示应用程序。
虽然我现在参与的项目中不会使用,但是…
-
- PostgreSQL
- MockServer(擬似HTTP Server)
我还制作了一个使用该工具进行测试的演示应用程序。
使用的东西
-
- Spring Boot 2.4.5
-
- Testcontainers 1.15.3
-
- JUnit 5
-
- Docker
-
- MySQL in Docker
-
- PostgreSQL in Docker
-
- Apache Kafka in Docker
-
- Cloud Pub/Sub Emulator in Docker
-
- Localstack(AWS Emulator) in Docker
- MockServer in Docker
做成的东西 de
该代码已经作为一个 GitHub 仓库公开发布。
- https://github.com/kazuki43zoo/spring-boot-test-demo-with-testcontainers
MySQL编程
使用MySQL作为数据库的应用程序测试配置如下。

-
- https://github.com/kazuki43zoo/spring-boot-test-demo-with-testcontainers/tree/main/mysql-test-demo
- https://www.testcontainers.org/modules/databases/mysql/
package com.example.demo.mysql;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest
@Testcontainers
class MysqlTestDemoApplicationTests {
@Container
private static final MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql"))
.withUsername("devuser")
.withPassword("devuser")
.withDatabaseName("devdb"); // MySQLのコンテナを生成
@Autowired
MysqlTestDemoApplication.MyMapper mapper;
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl); // コンテナで起動中のMySQLへ接続するためのJDBC URLをプロパティへ設定
}
@Test
void contextLoads() {
Assertions.assertThat(mysql.isRunning()).isTrue();
{
MysqlTestDemoApplication.Sample sample = new MysqlTestDemoApplication.Sample();
sample.id = 1;
sample.name = "Test";
mapper.create(sample); // データをMySQLへ追加
}
{
MysqlTestDemoApplication.Sample sample = mapper.findOne(1); // MySQLへ追加したデータを取得
Assertions.assertThat(sample.id).isEqualTo(1);
Assertions.assertThat(sample.name).isEqualTo("Test");
}
}
}
PostgreSQL编程
使用PostgreSQL作为数据库的应用程序测试配置如下。

-
- https://github.com/kazuki43zoo/spring-boot-test-demo-with-testcontainers/tree/main/postgresql-test-demo
- https://www.testcontainers.org/modules/databases/postgres/
package com.example.demo.postgresql;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest
@Testcontainers
class PostgresqlTestDemoApplicationTests {
@Container
private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres"))
.withUsername("devuser")
.withPassword("devuser")
.withDatabaseName("devdb"); // PostgreSQLのコンテナを生成
@Autowired
PostgresqlTestDemoApplication.MyMapper mapper;
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl); // コンテナで起動中のPostgreSQLへ接続するためのJDBC URLをプロパティへ設定
}
@Test
void contextLoads() {
Assertions.assertThat(postgres.isRunning()).isTrue();
{
PostgresqlTestDemoApplication.Sample sample = new PostgresqlTestDemoApplication.Sample();
sample.id = 1;
sample.name = "Test";
mapper.create(sample); // データをPostgreSQLへ追加
}
{
PostgresqlTestDemoApplication.Sample sample = mapper.findOne(1); // PostgreSQLへ追加したデータを取得
Assertions.assertThat(sample.id).isEqualTo(1);
Assertions.assertThat(sample.name).isEqualTo("Test");
}
}
}
阿帕奇·卡夫卡之编码
使用Apache Kafka作为消息中间件的应用程序的测试配置如下。

-
- https://github.com/kazuki43zoo/spring-boot-test-demo-with-testcontainers/tree/main/kafka-test-demo
- https://www.testcontainers.org/modules/kafka/
package com.example.demo.kafka;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.KafkaOperations;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
@SpringBootTest
@Testcontainers
class KafkaTestDemoApplicationTests {
@Container
static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka")); // Kafkaのコンテナを生成
@Autowired
KafkaOperations<?, ?> kafkaOperations;
@Autowired
BlockingQueue<Message<String>> messages;
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers); // コンテナで起動中のKafkaへ接続するための接続情報をプロパティへ設定
}
@Test
void contextLoads() throws InterruptedException {
Assertions.assertThat(kafka.isRunning()).isTrue();
kafkaOperations.send(MessageBuilder.withPayload("Hello!").build()); // Kafkaへメッセージを送信
Message<String> message = messages.poll(10, TimeUnit.SECONDS); // Kafkaから受信したメッセージを取得
Assertions.assertThat(message.getPayload()).isEqualTo("Hello!");
}
}
警告:
Topic的创建在KafkaTestDemoApplication中进行,但在生产环境中,应用程序启动时不会创建Topic,所以应该使用其他方法进行创建… 但由于在测试专用的Config文件中定义Bean会导致错误,所以暂时将其定义在KafkaTestDemoApplication中。
@SpringBootApplication
public class KafkaTestDemoApplication {
// …
@Bean
public NewTopic demoTopic() {
return TopicBuilder.name(“demoTopic”).build();
}
// …
}
云发布/订阅编程服务
使用GCP(Google Cloud Platform)的Cloud Pub/Sub作为消息代理,测试应用程序的配置如下所示。

-
- https://github.com/kazuki43zoo/spring-boot-test-demo-with-testcontainers/tree/main/gcp-pubsub-test-demo
- https://www.testcontainers.org/modules/gcloud/#pubsub
package com.example.demo.gcp.pubsub;
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.grpc.GrpcTransportChannel;
import com.google.api.gax.rpc.FixedTransportChannelProvider;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.cloud.pubsub.v1.SubscriptionAdminClient;
import com.google.cloud.pubsub.v1.SubscriptionAdminSettings;
import com.google.cloud.pubsub.v1.TopicAdminClient;
import com.google.cloud.pubsub.v1.TopicAdminSettings;
import com.google.cloud.spring.pubsub.PubSubAdmin;
import com.google.cloud.spring.pubsub.core.PubSubOperations;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PubSubEmulatorContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
@SpringBootTest
@Testcontainers
class GcpPubsubTestDemoApplicationTests {
@Container
static final PubSubEmulatorContainer pubsub = new PubSubEmulatorContainer(
DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:316.0.0-emulators")); // Cloud Pub/Sub Emulatorのコンテナを生成
@Autowired
PubSubOperations pubSubOperations;
@Autowired
BlockingQueue<String> messages;
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("spring.cloud.gcp.pubsub.emulator-host", pubsub::getEmulatorEndpoint); // コンテナで起動中のCloud Pub/Sub Enulatorへ接続するための接続情報をプロパティへ設定
}
@BeforeAll
static void setup() throws Exception {
ManagedChannel channel =
ManagedChannelBuilder.forTarget("dns:///" + pubsub.getEmulatorEndpoint())
.usePlaintext()
.build();
TransportChannelProvider channelProvider =
FixedTransportChannelProvider.create(GrpcTransportChannel.create(channel));
TopicAdminClient topicAdminClient =
TopicAdminClient.create(
TopicAdminSettings.newBuilder()
.setCredentialsProvider(NoCredentialsProvider.create())
.setTransportChannelProvider(channelProvider)
.build());
SubscriptionAdminClient subscriptionAdminClient =
SubscriptionAdminClient.create(
SubscriptionAdminSettings.newBuilder()
.setTransportChannelProvider(channelProvider)
.setCredentialsProvider(NoCredentialsProvider.create())
.build());
PubSubAdmin admin =
new PubSubAdmin(() -> "gcp-test", topicAdminClient, subscriptionAdminClient);
admin.createTopic("demoTopic"); // Topicの生成
admin.createSubscription("demoTopic-sub", "demoTopic"); // Subscriptionの生成
admin.close();
channel.shutdown();
}
@Test
void contextLoads() throws InterruptedException {
pubSubOperations.publish("demoTopic", "Hello World!"); // Pub/Sub Emulatorへメッセージを送信
String message = messages.poll(10, TimeUnit.SECONDS); // Pub/Sub Emulatorから受信したメッセージを取得
Assertions.assertThat(message).isEqualTo("Hello World!");
}
}
亚马逊云服务消息队列 (AWS SQS) 编辑
如果想要在AWS上实现分布式异步消息传递,是不是可以考虑使用SNS+SQS的组合呢?但是为了让讨论更简单,本帖将介绍基于SQS的应用程序测试配置。具体来说,配置如下所示。

-
- https://github.com/kazuki43zoo/spring-boot-test-demo-with-testcontainers/tree/main/aws-sqs-test-demo
- https://www.testcontainers.org/modules/localstack/
package com.example.demo.aws.sqs;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSAsync;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import io.awspring.cloud.messaging.core.QueueMessagingTemplate;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
@SpringBootTest
@Testcontainers
class AwsSqsTestDemoApplicationTests {
@Container
static final LocalStackContainer localstack = new LocalStackContainer(DockerImageName.parse("localstack/localstack"))
.withServices(LocalStackContainer.Service.SQS); // Mock SQSを有効にした状態でLocalStackコンテナ(Emulator)を作成
@Autowired
AmazonSQSAsync amazonSQSAsync;
@Autowired
BlockingQueue<Message<String>> messages;
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
AmazonSQS amazonSQS = AmazonSQSClientBuilder.standard()
.withEndpointConfiguration(localstack.getEndpointConfiguration(LocalStackContainer.Service.SQS))
.withCredentials(localstack.getDefaultCredentialsProvider())
.build();
amazonSQS.createQueue("demoQueue"); // Queueの作成
// ↓ コンテナで起動中のSQS Emulatorへ接続するための資格情報をプロパティへ設定
registry.add("cloud.aws.credentials.access-key", localstack::getAccessKey);
registry.add("cloud.aws.credentials.secret-key", localstack::getSecretKey);
// ↓ コンテナで起動中のSQS Emulatorへ接続するためのリージョン情報をプロパティへ設定
registry.add("cloud.aws.region.static", localstack::getRegion);
// ↓ コンテナで起動中のSQS Emulatorへ接続するための接続情報をプロパティへ設定
registry.add("cloud.aws.sqs.endpoint", localstack.getEndpointConfiguration(LocalStackContainer.Service.SQS)::getServiceEndpoint);
}
@Test
void contextLoads() throws InterruptedException {
QueueMessagingTemplate template = new QueueMessagingTemplate(amazonSQSAsync);
template.send("demoQueue", MessageBuilder.withPayload("Hello World!").build()); // SQS Emulatorへメッセージを送信
Message<String> message = messages.poll(10, TimeUnit.SECONDS); // SQS Emulatorから受信したメッセージを取得
Assertions.assertThat(message.getPayload()).isEqualTo("Hello World!");
}
}
嘲笑服务器编。
测试配置如下,用于访问其他服务提供的Web API(REST API)的应用程序。

-
- https://github.com/kazuki43zoo/spring-boot-test-demo-with-testcontainers/tree/main/httpclient-test-demo
- https://www.testcontainers.org/modules/mockserver/
package com.example.demo.httpclient;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest
@Testcontainers
class HttpclientTestDemoApplicationTests {
@Container
static final MockServerContainer mockServer =
new MockServerContainer(DockerImageName.parse("jamesdbloom/mockserver:mockserver-5.11.2")); // MockServerのコンテナを生成
MockServerClient mockServerClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort()); // MockServerへ応答データを設定するためのクライアントコンポーネント
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("api.baseUrl", () -> "http://" + mockServer.getHost() + ":" + mockServer.getServerPort()); // コンテナで起動中のMockServerへ接続するための接続情報をプロパティへ設定
}
@Autowired
HttpclientTestDemoApplication.MyClient myClient;
@Test
void contextLoads() {
mockServerClient
.when(HttpRequest.request().withPath("/hello"))
.respond(HttpResponse.response().withBody("Hello!")); // 応答データをMockServerへ設定
String message = myClient.hello(); // Web APIを呼び出す処理を実行
Assertions.assertThat(message).isEqualTo("Hello!");
}
}
注意:
以下是调用Web API的实现方式。@Component
class MyClient {
final WebClient client;
MyClient(WebClient.Builder builder, @Value(“${api.baseUrl:http://localhost:8080/}”) String baseUrl) {
this.client = builder.baseUrl(baseUrl).build();
}
String hello() {
return client.get()
.uri(“/hello”)
.exchangeToMono(res -> res.bodyToMono(String.class))
.block();
}
}
整理
在应用程序中使用Testcontainers可以轻松设置中间件。但是……由于测试的启动和停止速度较慢,因此我们应该注意到这一点。如果希望尽量减少启动和停止的影响,可以考虑采用Singleton模式。
- https://www.testcontainers.org/test_framework_integration/manual_lifecycle_control/#singleton-containers