従来 Testcontainers とspring-bootを組み合わせる場合は @DynamicPropertySource
で接続URLプロパティなどを置き換える処理がやや煩雑だった。しかし https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1 によると有名どころのdockerコンテナであればその必要性が無くなった、とある。今回はそれを試す。
ソースコード
まずは検証用の適当なアプリケーションを作成する。適当なテーブルから一行を返すだけのREST APIを作成する。
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.1.5' id 'io.spring.dependency-management' version '1.1.3' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '17' } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' runtimeOnly 'com.oracle.database.jdbc:ojdbc8' developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-webflux' testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:oracle-xe' // https://stackoverflow.com/questions/77241793/nosuchmethoderror-java-util-set-org-junit-platform-engine-testdescriptor-getan testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
検証用アプリケーション
package springtestsample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringTestSampleApplication { public static void main(String[] args) { SpringApplication.run(SpringTestSampleApplication.class, args); } }
package springtestsample.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; import springtestsample.entity.Users; import springtestsample.repository.UsersRepository; @RestController @RequiredArgsConstructor public class UserController { final UsersRepository users; @GetMapping("/user/{id}") public Users user(@PathVariable("id") String id) { return users.findById(id).orElse(new Users()); } }
package springtestsample.repository; import org.springframework.data.jpa.repository.JpaRepository; import springtestsample.entity.Users; public interface UsersRepository extends JpaRepository<Users, String> { }
package springtestsample.entity; import jakarta.persistence.Entity; import jakarta.persistence.Id; import lombok.Data; @Entity @Data public class Users { @Id String id; String name; }
テスト時には使わないが src/main/resources/application.properties
も一応載せておく。
spring.datasource.url jdbc:oracle:thin:@localhost:11521/FREEPDB spring.datasource.username system spring.datasource.password a
テストコード
package springtestsample.integration; import java.sql.SQLException; import javax.sql.DataSource; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; 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.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.http.MediaType; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.reactive.server.WebTestClient; import org.testcontainers.containers.OracleContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import springtestsample.SpringTestSampleApplication; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SpringTestSampleApplication.class) @Testcontainers @Sql(scripts = {"classpath:schema-test.sql", "classpath:data-test.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) class IntegrationTest { @LocalServerPort int port; @Container @ServiceConnection static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21.3.0-slim"); @Autowired WebTestClient client; @Test @DisplayName("ユーザ取得の正常系") void test() { client.get().uri("http://localhost:" + port + "/user/{id}", "kagami") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody().json(""" { "id":"kagami" , "name":"hoge" } """); } }
テスト開始時に実行されるSQL src/test/resources/schema-test.sql
, src/test/resources/data-test.sql
は以下。
create table users( id varchar (50) not null primary key , name varchar (500) not null );
insert into users(id, name) values ('kagami', 'hoge');
説明など
大まかには以下と同じ。
https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1 によると、@ServiceConnection
の付与と対応クラスであれば、この一行だけでdockerコンテナ開始・終了をやってくれる。
手元の環境だとOracleのコンテナは20秒弱で起動する。integration testでは十分な速度であろう。
他イメージは使用できない?
@ServiceConnection
の使用には Testcontainers 用のイメージを使う必要がある(と思う)。他に使用可能なイメージは https://hub.docker.com/r/gvenzl/oracle-xe にある。
Oracle公式のイメージ container-registry.oracle.com/database/free:23.2.0.0
も使用可能とは思うが@ServiceConnection
としては使用できなかった。ただ、エラーを見るに接続URLが無くてエラーが出てるだけっぽいので、こちらは従来通り @DynamicPropertySource
を使うやり方になるのだと思われる。
Oracle 23cは使用できない?
23c用のイメージ自体は https://hub.docker.com/r/gvenzl/oracle-free にあるが対応するtestcontainersの依存性が2023/11/08時点ではまだ無いっぽい。21までは Testcontainers :: JDBC :: Oracle XE を使い、23c以降用の依存性はまだ無いがソースコード https://github.com/testcontainers/testcontainers-java/blob/main/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleContainer.java はあるように見える。なので、そのうちアップロードされるのではないか、と思われる。
spring.datasource.urlは使わない?
テスト内で @Value("${spring.datasource.url}")
を取得すると src/main/resources/application.properties
の値になる。なので、プロパティの動的置換で DataSource
を設定してるわけでは無いらしい。