TestContainers使ってみないか?

みなさんは、DBアクセスのテストはどう書いていますか?DBをモックにしていますか?実際にDBを用意してテストしていますか? 私は、だいたいH2を用いてテストを書いています。H2を使っているとDDL問題に直面してしまいます。本番ではMySQLやPostgresを使っていると思うので、テストの為にDDLを用意しないといけません。 わざわざ用意するのめんどくさいとか、これって本当にプロダクトコードのテストになっているのか?とかの疑問を持っていました。

そこで、知り合ったのが今回紹介するTestContainersです。 TestContainersは、JUnitのテストをサポートするJavaのライブラリで、Dockerコンテナ上でDBやSelenium web browserなどを起動することができます。

今回紹介しているサンプルで使用している技術は次の通りです。

Java 11
Maven
Spring Boot 2.1.2.RELEASE
Spring Data JPA
MySQL
JUnit 5
TestContainers 1.10.6

TestContainersをとりあえず動かそう

pom.xmlにまず依存関係を追加します。

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>

HelloTest.java

@Testcontainers
class HelloTest {

    @Container
    private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();

    @Test
    void test() {
        assertTrue(MY_SQL_CONTAINER.isRunning());
    }
}

これを実行すると、次のログが出てDockerが起動した後にテストが実行されているのが確認できます。

...
00:43:22.656 [main] INFO org.testcontainers.DockerClientFactory - Connected to docker:
  Server Version: 18.09.1
  API Version: 1.39
  Operating System: Docker for Mac
  Total Memory: 1999 MB
...
00:43:35.506 [main] INFO org.testcontainers.DockerClientFactory - Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
        ℹ︎ Checking the system...
        ✔ Docker version should be at least 1.6.0
00:43:35.513 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 3ff9169b21b876bfc317476f8750851c18881855d46b1ed8a3656354a6f6487a,<null>,true,<null>,<null>,<null>,<null>,{df,-P},com.github.dockerjava.core.exec.ExecCreateCmdExec@a8e6492
00:43:35.610 [tc-okhttp-stream-812143047] DEBUG com.github.dockerjava.core.command.ExecStartResultCallback - STDOUT: Filesystem           1024-blocks    Used Available Capacity Mounted on
overlay               65792556   2810704  59610076   5% /
tmpfs                    65536         0     65536   0% /dev
tmpfs                  1023516         0   1023516   0% /sys/fs/cgroup
/dev/sda1             65792556   2810704  59610076   5% /etc/resolv.conf
/dev/sda1             65792556   2810704  59610076   5% /etc/hostname
/dev/sda1             65792556   2810704  59610076   5% /etc/hosts
shm                      65536         0     65536   0% /dev/shm
tmpfs                   204704       568    204136   0% /run/docker.sock
tmpfs                  1023516         0   1023516   0% /proc/acpi
tmpfs                    65536         0     65536   0% /proc/kcore
tmpfs                    65536         0     65536   0% /proc/keys
tmpfs                    65536         0     65536   0% /proc/timer_list
tmpfs                    65536         0     65536   0% /proc/sched_debug
tmpfs                  1023516         0   1023516   0% /sys/firmware
        ✔ Docker environment should have more than 2GB free disk space
...
00:46:59.144 [main] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
00:46:59.144 [main] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22
00:46:59.145 [main] DEBUG 🐳 [mysql:5.7.22] - Trying to start container: mysql:5.7.22 (attempt 1/3)
00:46:59.145 [main] DEBUG 🐳 [mysql:5.7.22] - Starting container: mysql:5.7.22
00:46:59.145 [main] INFO 🐳 [mysql:5.7.22] - Creating container for image: mysql:5.7.22
...
00:47:13.432 [ducttape-1] INFO 🐳 [mysql:5.7.22] - Obtained a connection to container (jdbc:mysql://localhost:32769/test)
00:47:13.434 [main] INFO 🐳 [mysql:5.7.22] - Container mysql:5.7.22 started
00:47:13.467 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,false,com.github.dockerjava.core.exec.InspectContainerCmdExec@77602954
00:47:13.468 [main] DEBUG com.github.dockerjava.core.exec.InspectContainerCmdExec - GET: OkHttpWebTarget(okHttpClient=org.testcontainers.shaded.okhttp3.OkHttpClient@6941827a, baseUrl=http://docker.socket/, path=[/containers/7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31/json], queryParams={})
00:47:13.479 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,<null>,com.github.dockerjava.core.exec.KillContainerCmdExec@73c60324
00:47:14.088 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,false,com.github.dockerjava.core.exec.InspectContainerCmdExec@71ae31b0
00:47:14.088 [main] DEBUG com.github.dockerjava.core.exec.InspectContainerCmdExec - GET: OkHttpWebTarget(okHttpClient=org.testcontainers.shaded.okhttp3.OkHttpClient@6941827a, baseUrl=http://docker.socket/, path=[/containers/7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31/json], queryParams={})
00:47:14.099 [main] DEBUG com.github.dockerjava.core.command.AbstrDockerCmd - Cmd: 7b65dc2dfe7e4bd414d9bf228e5b5a004d1045ed05d1cf01b203fba38a5a7c31,true,true,com.github.dockerjava.core.exec.RemoveContainerCmdExec@2c7d121c
00:47:14.151 [main] DEBUG org.testcontainers.utility.ResourceReaper - Removed container and associated volume(s): mysql:5.7.22Class transformation time: 0.043640464s for 3142 classes or 1.3889390197326542E-5s per class

ここを見てもらえばわかるのですが、テスト実行前にコンテナが正しく起動しているかチェックします。しかし、チェックするのでテスト終了に時間がかかってしまいます。 testcontainers.propertiesを置いて、checks.disable=true を設定するとこのテスト実行前のチェックが行われなくなります。

Repositoryのテスト書いてみよう

テスト対象のレポジトリクラス(BrandRepository.java

@Repository
public interface BrandRepository extends JpaRepository<Brand, Integer> {
    List<Brand> findAllByGender(Brand.Gender gender);
}

テスト用の設定ファイル(application.properties)

spring.datasource.url=jdbc:tc:mysql:5.7.22://hostname:port/test?TC_MY_CNF=db/mysql_conf_override
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=test
spring.datasource.password=
spring.datasource.sql-script-encoding=utf-8

spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

MySQL文字コードUTF-8に変更したいので、spring.datasource.urlでTC_MY_CNFパラメータでsomepath/mysql_conf_overrideを設定します。somepath/mysql_conf_overrideの下にある*.cnfファイルを読み込んでくれます。 今回は、db/mysql_conf_override/custom.cnfにcharacter-set-server = utf8だけを書きました。

テストクラス(BrandRepositorTest.java

@Testcontainers
@DataJpaTest(excludeAutoConfiguration = AutoConfigureTestDatabase.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BrandRepositoryTest {

    @Autowired
    private BrandRepository brandRepository;

    @Test
    @Sql(scripts = "classpath:/db/migration/brand/initial_data.sql")
    void findByGender() {
            Assertions.assertThat(brandRepository.findAllByGender(Brand.Gender.MAN))
                .extracting(Brand::getName, Brand::getDesigner, Brand::getGender)
                .containsExactly(Tuple.tuple("ETHOSENS", "橋本 唯", Brand.Gender.MAN));
    }
}

@DataJpaTest は、デフォルトでH2を使うので、その設定がされないようにします。 これで起動すると、application.propertiesの設定が使われて、Docker上に起動したMySQLが使われます。

TestContainersを使うことで本番と同じDBを簡単に用意できます。ドキュメントのボリュームは少ないので1時間半もあれば全部読み切ることができます。 ぜひ試してみてください。Go用JS用のTestContainersもあるようです。(未検証)

今回紹介したソースコードは、GitHubに置いています。