kagamihogeの日記

kagamihogeの日記です。

SpringBootTest + Testcontainersの親クラスで設定共通化による高速化

背景

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コンテナがよしなにやってくれるので不要。

参考URL