kagamihogeの日記

kagamihogeの日記です。

Testcontainers + Spring Boot 3.1 + Oracle XE

従来 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');

説明など

大まかには以下と同じ。

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 を使うやり方になるのだと思われる。

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 を設定してるわけでは無いらしい。