みなさんは、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もあるようです。(未検証)