はじめに
Testcontainers を使用すると、JUnit のテスト中だけ MySQL のコンテナを起動することができます。
SimpleMySQLTest.java というサンプルコードを見ると結構簡単に使えそうですが、Spring Boot + MyBatis で試してみたら結構苦労したので、どうすれば動くかまとめておきます。
まずは普通に Spring Boot + MyBatis を動かす
Testcontainers でテストを試す前に、動くコードを書いておきます。
MyBatis のコードを作成
UserRepositoryImpl.java、UserMapper.java、UserMapper.xml の 3 ファイルを作成します。
package springdockerexample.infrastructure.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import springdockerexample.domain.user.Name;
import springdockerexample.domain.user.User;
import springdockerexample.domain.user.UserRepository;
import springdockerexample.domain.user.Users;
import java.util.List;
@Repository
public class UserRepositoryImpl implements UserRepository {
@Autowired
private UserMapper mapper;
@Override
public Users findAll() {
List<User> users = mapper.selectAll();
return new Users(users);
}
}
package springdockerexample.infrastructure.user;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import springdockerexample.domain.user.Name;
import springdockerexample.domain.user.User;
import java.util.List;
@Mapper
public interface UserMapper {
List<User> selectAll();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="springdockerexample.infrastructure.user.UserMapper">
<resultMap id="user" type="springdockerexample.domain.user.User">
<result property="name.value" column="name"/>
<result property="age.value" column="age"/>
</resultMap>
<select id="selectAll" resultMap="user">
SELECT name, age FROM users
</select>
</mapper>
プロパティファイルを用意
DB の接続情報を application.yaml に書きます。
spring:
datasource:
url: jdbc:mysql://localhost/mydb
username: user
password: password
driverClassName: com.mysql.cj.jdbc.Driver
テストデータを用意
開発・テスト用のデータをファイルに用意します。
USE `mydb`;
CREATE TABLE `users` (
`name` VARCHAR(255) NOT NULL,
`age` int NOT NULL
);
INSERT INTO `users` (`name`, `age`) VALUES
('Alice', 20),
('Bob', 30);
ローカルでコンテナを起動
Spring Boot + MyBatis の動作確認のため、Docker Compose で MySQL を起動します。
このとき、先ほど用意した SQL ファイルを docker-entrypoint-initdb.d にマウントすることで、データが自動的に挿入されます。
version: '3'
services:
my-db:
image: mysql:5.7.25
ports:
- 3306:3306
volumes:
- ./src/test/resources/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: mydb
MYSQL_USER: user
MYSQL_PASSWORD: password
動作確認
spring-dev-tools を入れておけば、./mvnw spring-boot:run
で起動します。
$ ./mvnw spring-boot:run
$ curl localhost:8080/users
{"users":[{"name":"Alice","age":20},{"name":"Bob","age":30}]}
無事動作しています。
※ RestController なども書いていますが、記事への掲載は省略しました。
このように、Docker Compose で起動したコンテナでテストすることも可能ですが、それではテストケースごとにコンテナを起動し直したりすることはできません。
JUnit のテストの中で自由自在にコンテナを起動するため、Testcontainers を使います。
Testcontainers で JUnit 実行中だけコンテナを起動
テスト用の MySQL 起動 + 接続情報設定
「testcontainersで使い捨てのデータベースコンテナを用意してSpring Bootアプリケーションのテストをおこなう」を参考に、テスト用の MySQL コンテナの起動と Context への接続情報設定のため、以下のファイルを作成します。
package springdockerexample.testhelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
public class MySQLContainerContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final String MYSQL_IMAGE = "mysql:5.7.25";
private static final String DATABASE_NAME = "mydb";
private static final String USERNAME = "user";
private static final String PASSWORD = "password";
private static final int PORT = 3306;
private static final String INIT_SQL = "docker-entrypoint-initdb.d/init.sql";
private static final String INIT_SQL_IN_CONTAINER = "/docker-entrypoint-initdb.d/init.sql";
private static final Logger LOGGER = LoggerFactory.getLogger(MySQLContainerContextInitializer.class);
private static final MySQLContainer MYSQL = (MySQLContainer) new MySQLContainer(MYSQL_IMAGE)
.withDatabaseName(DATABASE_NAME)
.withUsername(USERNAME)
.withPassword(PASSWORD)
.withExposedPorts(PORT)
.withLogConsumer(new Slf4jLogConsumer(LOGGER))
.withClasspathResourceMapping(INIT_SQL, INIT_SQL_IN_CONTAINER, BindMode.READ_ONLY);
static {
MYSQL.start();
}
@Override
public void initialize(ConfigurableApplicationContext context) {
String mysqlJdbcUrl = MYSQL.getJdbcUrl();
TestPropertyValues.of("spring.datasource.url=" + mysqlJdbcUrl)
.applyTo(context.getEnvironment());
}
}
この内容を順に説明していきます。
MySQL のコンテナの設定
private static final MySQLContainer MYSQL = (MySQLContainer) new MySQLContainer(MYSQL_IMAGE)
.withDatabaseName(DATABASE_NAME)
.withUsername(USERNAME)
.withPassword(PASSWORD)
.withExposedPorts(PORT)
.withLogConsumer(new Slf4jLogConsumer(LOGGER))
.withClasspathResourceMapping(INIT_SQL, INIT_SQL_IN_CONTAINER, BindMode.READ_ONLY);
ここでは、先ほど docker-compose.yaml で書いていた内容とほとんど同じ設定を Java で記述しています。
new MySQLContainer(MYSQL_IMAGE) でイメージ名を指定し、データベース名やユーザ名、パスワードといった設定もここで行なっています。
また、withClasspathResourceMapping で DB の初期化用の SQL をマウントしています。
同様にして、 MySQL の設定ファイルもマウントできるようです。
MySQL のコンテナ起動
static {
MYSQL.start();
}
MySQL のコンテナを起動しています。
MySQL.start() は後述する initialize 内の MySQL.getJdbUrl() より先に実行される必要があります。
この例ではサボっていますが、終了時に MySQL.stop() を呼び出したほうがお行儀がいいと思われます。
接続情報の設定
Testcontainers で起動した MySQL に接続するためのポートは動的に割り当てられるため、それを踏まえて動的に接続情報を設定する必要があります。
MySQLContainerContextInitializer というクラスを作成したのは、Spring Bootの設定を動的に変更するためです。
具体的な設定方法は以下の通りです。
@Override
public void initialize(ConfigurableApplicationContext context) {
String mysqlJdbcUrl = MYSQL.getJdbcUrl();
TestPropertyValues.of("spring.datasource.url=" + mysqlJdbcUrl)
.applyTo(context.getEnvironment());
}
ちなみに、MySQL への接続情報を MySQLContainer インスタンスから取得する必要があることは、SimpleMySQLTest.java の以下の部分からも分かります。
@NonNull
protected ResultSet performQuery(MySQLContainer containerRule, String sql) throws SQLException {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDriverClassName(containerRule.getDriverClassName());
hikariConfig.setJdbcUrl(containerRule.getJdbcUrl());
hikariConfig.setUsername(containerRule.getUsername());
hikariConfig.setPassword(containerRule.getPassword());
HikariDataSource ds = new HikariDataSource(hikariConfig);
Statement statement = ds.getConnection().createStatement();
statement.execute(sql);
ResultSet resultSet = statement.getResultSet();
resultSet.next();
return resultSet;
}
テストの記述
Spirng Boot のテストを通常通り記載します。
package springdockerexample.infrastructure.user;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import springdockerexample.domain.user.UserRepository;
import springdockerexample.domain.user.Users;
import springdockerexample.testhelper.MySQLContainerContextInitializer;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(initializers = { MySQLContainerContextInitializer.class })
public class UserRepositoryImplTest {
@Autowired
UserRepository userRepository;
@Test
public void test() {
Users users = userRepository.findAll();
assertThat(users.count(), is(2));
}
}
ポイントは、@ContextConfiguration(initializers = { MySQLContainerContextInitializer.class })
で先ほど作成したクラスを指定し、MySQL の起動と接続情報の設定を行なっているところです。
あとは普通の JUnit のテストを記述すれば、問題なく DB にアクセスできます。
まとめ
まとめると全然難しくないのですが、実際には結構苦労しました。
セットアップし終えてしまえば便利に使えるかもしれません。
Testcontainers は DB 以外にも任意のコンテナに対応しているので、自由自在な自動テストが可能になります。
なお、この記事の内容の最終的なファイル構成はおおよそ以下のようになっています。
$ tree
.
├── pom.xml
├── ...
└── src
├── main
│ ├── java
│ │ └── springdockerexample
│ │ ├── SpringDockerExampleApplication.java
│ │ ├── ...
│ │ └── infrastructure
│ │ └── user
│ │ ├── UserMapper.java
│ │ └── UserRepositoryImpl.java
│ └── resources
│ ├── application.yaml
│ └── springdockerexample
│ └── infrastructure
│ └── user
│ └── UserMapper.xml
└── test
├── java
│ └── springdockerexample
│ ├── SpringDockerExampleApplicationTests.java
│ ├── infrastructure
│ │ └── user
│ │ └── UserRepositoryImplTest.java
│ └── testhelper
│ └── MySQLContainerContextInitializer.java
└── resources
└── docker-entrypoint-initdb.d
└── init.sql
ソースコードは こちら です。