従来 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'
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');
説明など
大まかには以下と同じ。
kagamihoge.hatenablog.com
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
を使うやり方になるのだと思われる。
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
を設定してるわけでは無いらしい。