Spring BootのService Connection を使ったTest containersの活用が全然浸透していないように思える。未だにTestcontainersをテストでしか使っていない場合は↓を見てほしい。https://t.co/awwtvJ34vk
— Toshiaki Maki (🦋 @ik.am) (@making) 2025年8月30日
spring-bootとTestcontainersの組み合わせにより、開発と結合テストでRDBなど各種ミドルウェアのセットアップを共有できる。とりあえず試すには、Spring InitializrでOracle等ミドルウェアも一緒に起動する開発・結合テストのサンプルコードを含むプロジェクトを生成できる。
チュートリアル相当
以降の手順の前提条件はtestcontainersからdockerが起動可能な設定済みなこと。
まずSpring InitializrでTestcontainersを含むプロジェクトを生成する。ここでは自分の趣味でOracleを使うのでweb, jdbc, testcontainers, oracleをadd dependenciesから追加する。
exploreを見ると、mainを記述するDempApplication
以外にsrc/test/java
にいくつかのサンプルクラスが追加されている。
以下のソースコードは特に断りの無い限り生成されたものをそのまま載せている。
以下はbuild.gradle
。
plugins { id 'java' id 'org.springframework.boot' version '3.5.5' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.example' version = '0.0.1-SNAPSHOT' description = 'Demo project for Spring Boot' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' 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-free' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
以下のクラス群はsrc/test/java
下のもの。src/main
では無い点に注意。
import org.springframework.boot.SpringApplication; public class TestDemoApplication { public static void main(String[] args) { SpringApplication.from(DemoApplication::main).with(TestcontainersConfiguration.class).run(args); } }
@Import(TestcontainersConfiguration.class) @SpringBootTest class DemoApplicationTests { @Test void contextLoads() { } }
import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.testcontainers.oracle.OracleContainer; import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) class TestcontainersConfiguration { @Bean @ServiceConnection OracleContainer oracleFreeContainer() { // return new OracleContainer(DockerImageName.parse("gvenzl/oracle-free:latest")); return new OracleContainer(DockerImageName.parse("gvenzl/oracle-free:23.5-faststart")); } }
上記はdocker imageを取得済みのものに修正している(時間短縮のため)。
これでsrc/test/java
のcom.example.demo.TestDemoApplication
を起動する。正常に起動すれば、testcontainers経由でdokcerにoracleインスタンスが起動した上でspring-bootも起動する。以下はログの一部。
tc.gvenzl/oracle-free:23.5-faststart : Container is started (JDBC URL: jdbc:oracle:thin:@localhost:32771/freepdb1) o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
dockerから見てもoracleが起動しているのが分かる。spring-bootを終了するとこのコンテナも削除される。
docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1391efbea242 gvenzl/oracle-free:23.5-faststart "container-entrypoin…" 2 minutes ago Up 2 minutes 0.0.0.0:32771->1521/tcp, :::32771->1521/tcp distracted_golick 2f8e577a1bbf testcontainers/ryuk:0.12.0 "/bin/ryuk" 2 minutes ago Up 2 minutes 0.0.0.0:32770->8080/tcp, :::32770->8080/tcp testcontainers-ryuk-43ca0794-883b-40c6-8d0d-bfea5d7a69a3
また、DemoApplicationTests
が結合テストのサンプルコードとなる。こちらも起動すると同様にdockerでoracleが起動してspring-bootが起動してテストを実行してコンテナ削除、という流れになる。
というわけで、Spring Initializrで生成するとoracleとかも一緒に起動するアプリケーションと結合テストコードのサンプルプロジェクトが入手可能になった。とりあえず起動するだけなら、docker run ..
してspring.datasource.*
を設定して、が不要になった。
色々試す
oracle起動時にテーブル作成して初期データ投入
以下はtestcontainersと直接関係は無いが、もう少しコードを追加して良くありそうな開発環境を考える。追加コードの内容は、oracle起動時にテーブル作成して初期データも投入する。
src/test/resources/application.properties
に常にDB初期化実行する設定を追加。
spring.sql.init.mode=always
src/test/resources/
下にテーブル生成のschema.sql
と初期データ投入用のdata.sql
を配置する。
CREATE TABLE test_users( id VARCHAR2(50) NOT NULL , value NUMBER NOT NULL , CONSTRAINT test_users_pk PRIMARY KEY (id) );
insert into test_users(id, value) values ('1', 123);
src/main
側に適当な動作確認用にSQL実行を伴うRestController
を追加する。
@RestController @SpringBootApplication public class DemoApplication { final DataSource dataSource; public DemoApplication(DataSource dataSource) { this.dataSource = dataSource; } @GetMapping("/sample") String sample() { JdbcClient jdbc = JdbcClient.create(dataSource); return jdbc.sql("select count(*) from test_users").query(Integer.class).single().toString(); } public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
結合テストとしてsrc/test/java
下のDemoApplicationTests
を下記のように修正する。上で追加した/sample
を実行してレスポンスをassertで検証するように修正。
import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; import org.springframework.web.client.RestClient; @Import(TestcontainersConfiguration.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class DemoApplicationTests { @LocalServerPort int port; @Test void contextLoads() { RestClient client = RestClient.builder().baseUrl("http://localhost:" + port).build(); assertEquals("1", client.get().uri("/sample").retrieve().body(String.class)); } }
これにより、spring-bootを起動してブラウザや適当なweb-apiクライアントから動作確認したい場合はTestDemoApplication
から、結合テストしたい場合はDemoApplicationTests
から、その両者ともTestcontainers経由でoracleを起動することでミドルウェア設定を共有できる。
devtoolsでコンテナ起動したままspring-bootだけ再起動
上述までの設定で開発をするとspring-bootを停止・起動するたびにコンテナ削除・生成され、oracleのようにまぁまぁ重いコンテナだとまぁまぁだるい。解決案の一つに https://docs.spring.io/spring-boot/reference/using/devtools.html devtoolsがある。
https://docs.spring.io/spring-boot/reference/features/dev-services.html#features.dev-services.testcontainers.at-development-time.devtools にあるように、@ServiceConnection
に@RestartScope
を付与する。
@Bean @ServiceConnection @RestartScope OracleContainer oracleFreeContainer() { //...
Spring Initializrで生成してるとspring-boot-devtools
がdevelopmentOnly
なのでこれをtestAndDevelopmentOnly
に変更する。
// developmentOnly 'org.springframework.boot:spring-boot-devtools' testAndDevelopmentOnly 'org.springframework.boot:spring-boot-devtools'
なおspring.sql.init.mode=always
にしてるとSQL類も再実行されてしまう。この辺は色々やりようはあると思う。例えば、CREATE TABLE IF NOT EXISTS
にしたり、初期化データは毎回truncate table ..
したり、とか。テストだけ@SpringBootTest(properties = {"spring.sql.init.mode=always"})
にして、開発はまた別途やり方を考えるとか。
開発時の工夫
色々工夫すると色々便利になりそうだが、どこまでやるかはプロジェクト事情や自分の好みに寄るかな、という感じはする。結合テストはともかく開発の方は特に。俺自身はまだこの機能を実戦投入してないんで何とも言えないけど。
たとえば、コンテナ自身や初期化処理が重い場合はwithReuse(true)
でコンテナを再利用したくなると。しばらくの間はこのコンテナのままで開発続けたいなーーみたいな。ただ、キャッシュを長期間残すと別の問題が出やすくなり、意図しないコンテナが残存していて妙な動作になってしまった……も考えられる。なのでspring-boot起動するたびにコンテナ全部再作成でも良い気はしないでもないが、それがどのくらいだるいかは場合に依るし個人の好みにも依るし。
Testcontainersで共有します、開発はプロジェクトでおおよそ標準的なやり方を定めます、その上で個人で工夫したいならしてね、になるかなぁ……などと思ったりした。
感想とか
checkoutしてspirng-boot起動するだけでoracleとか必要なミドルウェアが全部起動して初期データも投入された状態で開発や結合テスト実行できる。Testcontainersで開発と結合テストの環境設定を共有するっていうアイデアは素晴らしい。