背景
Testcontainersでspring-bootのテストケースが増えると複数のクラスに分割したくなる。しかし、単純に複数のテストクラスを作成しただけだと、コンテナ(とspring-bootも)も複数個起動してしまいテストの実行時間が増えてしまう。
よって、Testcontainers関連の設定を一か所に共通化しつつ、コンテナを一度だけ起動する事で実行時間も減少させたい。
Singleton Containers
このような場合、TestcontainersのドキュメントによるとSingleton Containers Patternの使用を推奨している。ドキュメントの該当箇所は以下の通り。
https://testcontainers.com/guides/testcontainers-container-lifecycle/
ソースコード
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.3.0' id 'io.spring.dependency-management' version '1.1.5' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' runtimeOnly 'com.oracle.database.jdbc:ojdbc11' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:oracle-xe' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
src\test\resources\create_table.sql
CREATE TABLE emp (emp_no VARCHAR2(4));
以下がテストクラスの共通設定となる親クラス。
import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.testcontainers.containers.OracleContainer; @SpringBootTest abstract class IntegrationTestBase { @ServiceConnection static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21.3.0-slim-faststart") .withInitScript("create_table.sql"); static { oracle.start(); } @Autowired DataSource ds; }
以下は1つ目のspring-boot-testクラス。
import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.simple.JdbcClient; class SpringBootTest001 extends IntegrationTestBase { @Test void test() { Integer v = JdbcClient.create(ds).sql("select count(*) from emp").query(Integer.class).single(); System.out.println("test001 " + v); } }
以下は2つ目のspring-boot-testクラス。
import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.simple.JdbcClient; class SpringBootTest002 extends IntegrationTestBase { @Test void test() { Integer v = JdbcClient.create(ds).sql("select count(*) from emp").query(Integer.class).single(); System.out.println("test002 " + v); } }
以下は実行時のログ。コンテナの起動も一度、spring-bootの起動も一度になっているのが分かる。
20:22:04.678 [Test worker] INFO tc.gvenzl/oracle-xe:21.3.0-slim-faststart - Creating container for image: gvenzl/oracle-xe:21.3.0-slim-faststart 20:22:04.714 [Test worker] INFO tc.gvenzl/oracle-xe:21.3.0-slim-faststart - Container gvenzl/oracle-xe:21.3.0-slim-faststart is starting: c75caacbfeb6626ad6edbe90adfc135c54d20219e6bf4a5ce0deb08a2c0540ea 20:22:39.238 [Test worker] INFO tc.gvenzl/oracle-xe:21.3.0-slim-faststart - Container gvenzl/oracle-xe:21.3.0-slim-faststart started in PT34.5593481S 20:22:39.238 [Test worker] INFO tc.gvenzl/oracle-xe:21.3.0-slim-faststart - Container is started (JDBC URL: jdbc:oracle:thin:@localhost:32816/xepdb1) 20:22:39.278 [Test worker] INFO org.testcontainers.ext.ScriptUtils - Executing database script from create_table.sql 20:22:40.050 [Test worker] INFO org.testcontainers.ext.ScriptUtils - Executed database script from create_table.sql in 767 ms. 20:22:40.320 [Test worker] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration org.example.app.SpringTestSampleApplication for test class org.example.app.sharecontext.SpringBootTest001 . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.3.0) 20:22:41.003 [Test worker] INFO org.example.app.sharecontext.SpringBootTest001 - Starting SpringBootTest001 using Java 17.0.9 with PID 19900 (started by kagam in C:\Users\kagam\Documents\intellj-ws\signlerepo2\testcontainersss2) 20:22:41.004 [Test worker] INFO org.example.app.sharecontext.SpringBootTest001 - No active profile set, falling back to 1 default profile: "default" 20:22:41.570 [Test worker] INFO org.example.app.sharecontext.SpringBootTest001 - Started SpringBootTest001 in 0.889 seconds (process running for 39.087) Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended 20:22:42.229 [Test worker] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... 20:22:42.229 [Test worker] WARN com.zaxxer.hikari.util.DriverDataSource - Registered driver with driverClassName=oracle.jdbc.driver.OracleDriver was not found, trying direct instantiation. 20:22:42.282 [Test worker] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@69372c1e 20:22:42.282 [Test worker] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. test001 0 20:22:42.379 [Test worker] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [org.example.app.sharecontext.SpringBootTest002]: SpringBootTest002 does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 20:22:42.388 [Test worker] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration org.example.app.SpringTestSampleApplication for test class org.example.app.sharecontext.SpringBootTest002 test002 0 20:22:42.417 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated... 20:22:42.474 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed. BUILD SUCCESSFUL in 40s 12 actionable tasks: 2 executed, 10 up-to-date 20:22:42: Execution finished ':testcontainersss2:test'.
説明とか
A common misconfiguration of Singleton Containers - Testcontainers によると、@Testcontainers
や@Container
は各テストクラスごとにコンテナ起動・停止するので、以降のテストクラスは停止済みのコンテナに接続しようとして失敗してしまう。
これの回避には、static初期化子 or @BeforeAll
でコンテナを手動で一度だけ開始する。停止はryukコンテナがよしなにやってくれるので不要。