1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[JUnit/Spock] DB Tester: アノテーションとCSVでデータ投入から検証まで完結するDBテストフレームワーク

Last updated at Posted at 2025-12-10

はじめに

データベースを利用するアプリケーションのテストでは、テストデータの準備、テスト後の状態検証、複数テストケース間でのデータ分離など、考慮すべき事項が多くあります。

筆者自身、これらの作業を繰り返す中で「アノテーションを付けるだけでCSVからデータを投入し、結果を検証できれば便利なのでは」と考え、JUnitおよびSpockに対応したデータベーステストフレームワーク「DB Tester」を作成しました。本記事では、このライブラリの機能と使い方を紹介します。

特徴

Convention over Configuration

テストクラス名・パッケージ名に基づいてCSVファイルを自動的に解決します。明示的なパス指定は不要です。

src/test/resources/
└── com/example/UserRepositoryTest/
    ├── USERS.csv              # @Preparationで読み込まれる
    └── expected/
        └── USERS.csv          # @Expectationで比較される

宣言的なテスト記述

@Preparation@Expectationアノテーションを付与するだけで、テストデータの準備と検証を行います。アノテーションはクラスレベルまたはメソッドレベルに付与できます。

@ExtendWith(DatabaseTestExtension.class)
@Preparation  // 全テストメソッドに適用
@Expectation
class UserRepositoryTest {

    @Test
    void shouldCreateUser() {
        userRepository.create(new User("john", "john@example.com"));
    }

    @Test
    @Preparation(operation = Operation.INSERT)  // このメソッドのみ上書き
    void shouldUpdateUser() {
        userRepository.update(new User(1, "updated", "updated@example.com"));
    }
}

シナリオベースのテスト

[Scenario]列を使用して、1つのCSVファイルから複数のテストケースに対応するデータを抽出できます。テストメソッド名がシナリオ名として使用されます。

[Scenario],ID,NAME,EMAIL
shouldCreateUser,1,existing,existing@example.com
shouldUpdateUser,1,target,target@example.com
shouldDeleteUser,1,delete_me,delete@example.com

プログラマティックAPI

アノテーションベースの検証に加えて、テスト中に任意のタイミングでデータベースの状態を検証できます。特定のカラムを無視した比較や、SQLクエリ結果の検証が可能です。

// 特定カラムを無視して比較
DatabaseAssertion.assertEqualsIgnoreColumns(
    expectedTable, actualTable, "CREATED_AT", "UPDATED_AT");

// SQLクエリ結果を検証
DatabaseAssertion.assertEqualsByQuery(
    expectedTable, dataSource, "USERS",
    "SELECT * FROM USERS WHERE STATUS = 'ACTIVE'");

複数DataSource対応

1つのテストで複数のデータベースに対する操作と検証が可能です。

@BeforeAll
static void setUp(ExtensionContext context) {
    DataSourceRegistry registry = DatabaseTestExtension.getRegistry(context);
    registry.registerDefault(primaryDataSource);
    registry.register("secondary", secondaryDataSource);
}

@Test
@Preparation(dataSets = @DataSet(dataSourceName = "secondary"))
void shouldUseSecondaryDatabase() {
    // secondaryデータベースに対するテスト
}

インストール

Gradle

build.gradle.kts
testImplementation(platform("io.github.seijikohara:db-tester-bom:0.1.0"))
testImplementation("io.github.seijikohara:db-tester-junit")

Maven

pom.xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.seijikohara</groupId>
            <artifactId>db-tester-bom</artifactId>
            <version>0.1.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependency>
    <groupId>io.github.seijikohara</groupId>
    <artifactId>db-tester-junit</artifactId>
    <scope>test</scope>
</dependency>

モジュール選択

ユースケース モジュール
JUnit db-tester-junit
JUnit + Spring Boot db-tester-junit-spring-boot-starter
Spock db-tester-spock
Spock + Spring Boot db-tester-spock-spring-boot-starter

基本的な使用方法

1. テストクラスの作成

UserRepositoryTest.java
@ExtendWith(DatabaseTestExtension.class)
@Preparation  // クラス内の全テストメソッドに適用
@Expectation
class UserRepositoryTest {

    @BeforeAll
    static void setUp(ExtensionContext context) {
        DataSource dataSource = createDataSource();
        DatabaseTestExtension.getRegistry(context).registerDefault(dataSource);
    }

    @Test
    void shouldCreateUser() {
        userRepository.create(new User("john", "john@example.com"));
    }

    @Test
    void shouldUpdateUser() {
        userRepository.update(new User(1, "updated", "updated@example.com"));
    }
}

2. CSVファイルの作成

[Scenario]列でテストメソッドごとにデータを分離します。

USERS.csv
[Scenario],ID,NAME,EMAIL
shouldCreateUser,1,existing,existing@example.com
shouldUpdateUser,1,target,target@example.com
expected/USERS.csv
[Scenario],ID,NAME,EMAIL
shouldCreateUser,1,existing,existing@example.com
shouldCreateUser,2,john,john@example.com
shouldUpdateUser,1,updated,updated@example.com
  • shouldCreateUserテスト: 既存ユーザー1件の状態から、新規ユーザーを作成し、2件になることを検証
  • shouldUpdateUserテスト: ユーザー1件の状態から、そのユーザーを更新し、更新後の値を検証

アノテーション

@Preparation

テスト実行前にCSVデータをデータベースに投入します。

属性 説明 デフォルト値
dataSets 適用するデータセットの配列 空(規約ベースの検出)
operation データベース操作の種類 CLEAN_INSERT
tableOrdering テーブル処理順序の戦略 AUTO
// クラスレベル: 全テストメソッドに適用
@Preparation
class UserRepositoryTest { ... }

// メソッドレベル: 特定のテストメソッドに適用
@Test
@Preparation(operation = Operation.INSERT)
void shouldCreateUser() { ... }

// データセットの明示的な指定
@Preparation(dataSets = @DataSet(
    resourceLocation = "data/users",
    scenarioNames = {"testCase1"}
))

@Expectation

テスト実行後にデータベースの状態をCSVファイルと比較検証します。

属性 説明 デフォルト値
dataSets 検証するデータセットの配列 空(規約ベースの検出)
tableOrdering テーブル処理順序の戦略 AUTO
// クラスレベル: 全テストメソッドに適用
@Expectation
class UserRepositoryTest { ... }

// メソッドレベル: 特定のテストメソッドに適用
@Test
@Expectation(tableOrdering = TableOrderingStrategy.ALPHABETICAL)
void shouldCreateUser() { ... }

両アノテーションは@Inheritedが付与されているため、サブクラスにも継承されます。共通のテスト設定を基底クラスに定義することで、コードの重複を削減できます。

@DataSet

データセットの詳細な設定を行います。

属性 説明
resourceLocation データセットディレクトリのパス
scenarioNames 適用するシナリオ名
dataSourceName 対象のDataSource名
リソース指定の形式
形式 説明
クラスパス相対 data/users テストクラスパスからの相対パス
クラスパスプレフィックス classpath:data/users 明示的なクラスパス解決
絶対パス /tmp/testdata ファイルシステムの絶対パス
空文字列 "" 規約ベースの自動検出(デフォルト)

データベース操作

@Preparationoperation属性で指定できる操作の一覧です。

操作 説明 ユースケース
NONE 操作なし 読み取り専用の検証
INSERT 新規行を挿入 空テーブルへの追加
UPDATE 主キーで既存行を更新 既存データの変更
REFRESH Upsert(更新または挿入) 混合シナリオ
DELETE 主キーで指定行を削除 選択的な削除
DELETE_ALL 全行削除 シーケンスを保持したクリア
TRUNCATE_TABLE テーブルをTruncate シーケンスリセット付きクリア
CLEAN_INSERT 全削除後に挿入(デフォルト) 標準的なテスト準備
TRUNCATE_INSERT Truncate後に挿入 シーケンスリセット付きセットアップ

データファイル形式

CSVとTSVの2つの形式をサポートしています。

形式 拡張子 区切り文字 デフォルト
CSV .csv カンマ (,) Yes
TSV .tsv タブ (\t) No

形式の切り替えはConventionSettingsで設定します:

var conventions = ConventionSettings.standard()
    .withDataFormat(DataFormat.TSV);

基本構造

1行目はヘッダー行として扱われます。[Scenario]列はシナリオフィルタリングに使用されます。

[Scenario],ID,NAME,EMAIL,CREATED_AT
shouldCreateUser,1,john,john@example.com,2024-01-01 10:00:00

データ型の表現

表現 意味
空フィールド NULL値
空のクォート ("") 空文字列
2024-01-01 日付型(DATE)
2024-01-01 10:00:00 タイムスタンプ型(TIMESTAMP)
true / false 真偽値(BOOLEAN)
Base64文字列 BLOB型
ID,NAME,DESCRIPTION,CREATED_AT,IS_ACTIVE
1,test,,2024-01-01 10:00:00,true
2,empty,"",2024-01-01 11:00:00,false

上記の例では:

  • 1行目のDESCRIPTIONNULL
  • 2行目のDESCRIPTION空文字列

テーブル処理順序

外部キー制約を考慮して、テーブルの処理順序を制御できます。

TableOrderingStrategy

@Preparationおよび@ExpectationtableOrdering属性で順序決定の戦略を指定できます。

戦略 説明
AUTO 自動決定(デフォルト)
LOAD_ORDER_FILE load-order.txtを使用(必須)
FOREIGN_KEY JDBCメタデータで外部キー依存関係を解決
ALPHABETICAL テーブル名でアルファベット順にソート

AUTO戦略では以下の優先順位で順序を決定します:

  1. load-order.txtが存在する場合はそれを使用
  2. JDBCメタデータから外部キー依存関係を解決
  3. アルファベット順にフォールバック

外部キー自動解決

DB Testerはデータベースメタデータから外部キー関係を自動的に取得し、テーブルの処理順序を決定します。親テーブル(参照される側)が子テーブル(外部キーを持つ側)より先に処理されるため、外部キー制約違反を防ぐことができます。

この機能はJDBCのDatabaseMetaData.getExportedKeys()を使用して実装されており、特別な設定は不要です。

循環参照が検出された場合は警告をログに出力し、データセット宣言順が維持されます。

load-order.txt

データセットディレクトリにload-order.txtを配置することで、テーブルの処理順序を明示的に指定できます。

load-order.txt
# 親テーブルを先に記述
USERS
CATEGORIES

# 子テーブルは親テーブルの後に記述
ORDERS
ORDER_ITEMS

load-order.txtが存在しない場合、自動生成は行われません。明示的に必要な場合はTableOrderingStrategy.LOAD_ORDER_FILEを指定してください(ファイルが見つからない場合はエラーになります)。

操作別の処理順序
操作 処理順序
INSERT, REFRESH 親テーブル → 子テーブル(順方向)
DELETE, DELETE_ALL 子テーブル → 親テーブル(逆順)
TRUNCATE_TABLE 子テーブル → 親テーブル(逆順)
CLEAN_INSERT DELETE(逆順)→ INSERT(順方向)
TRUNCATE_INSERT TRUNCATE(逆順)→ INSERT(順方向)

フレームワーク統合

Spring Boot + JUnit

@SpringBootTest
@ExtendWith(SpringBootDatabaseTestExtension.class)
@Preparation
@Expectation
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldCreateUser() {
        userRepository.save(new User("john", "john@example.com"));
    }
}

Spring Boot Starterを使用する場合、DataSourceは自動的に登録されます。

Spock Framework

Spock Frameworkでは@DatabaseTestアノテーションを使用して拡張機能を有効化します。dbTesterRegistryプロパティを提供する必要があります。

UserRepositorySpec.groovy
@DatabaseTest
class UserRepositorySpec extends Specification {

    @Shared
    DataSource dataSource

    @Shared
    DataSourceRegistry registry

    // フレームワークが使用するプロパティアクセサ
    DataSourceRegistry getDbTesterRegistry() {
        return registry
    }

    def setupSpec() {
        dataSource = createDataSource()
        registry = new DataSourceRegistry()
        registry.registerDefault(dataSource)
    }

    @Preparation
    @Expectation
    def "should create user"() {
        when:
        userRepository.create(new User("john", "john@example.com"))

        then:
        noExceptionThrown()
    }
}

Spock + Spring Boot

@SpringBootTest
@SpringBootDatabaseTest
class UserRepositorySpec extends Specification {

    @Autowired
    UserRepository userRepository

    @Preparation
    @Expectation
    def "should create user"() {
        when:
        userRepository.save(new User("john", "john@example.com"))

        then:
        noExceptionThrown()
    }
}

対応データベース

以下のデータベースで動作確認済みです。

  • H2
  • MySQL
  • PostgreSQL
  • Oracle
  • SQL Server
  • Apache Derby
  • HSQLDB

標準JDBCを使用しているため、JDBC対応のデータベースであれば動作します。

アサーションメッセージ

期待値の検証が失敗した場合、フレームワークはすべての差分を収集し、人間が読みやすい形式で報告します。

Assertion failed: 3 differences in USERS, ORDERS
summary:
  status: FAILED
  total_differences: 3
tables:
  USERS:
    differences:
      - path: row_count
        expected: 3
        actual: 2
  ORDERS:
    differences:
      - path: "row[0].STATUS"
        expected: COMPLETED
        actual: PENDING
        column:
          type: VARCHAR(50)
          nullable: true
      - path: "row[1].AMOUNT"
        expected: 100.00
        actual: 99.99
        column:
          type: "DECIMAL(10,2)"

出力は有効なYAML形式のため、CI/CDパイプラインでの解析にも利用できます。

サンプルプロジェクト

GitHubリポジトリに各種サンプルが用意されています。

サンプル 説明
基本的な使用方法 @Preparation@Expectationの基本的な使い方
シナリオフィルタリング [Scenario]列を使用したテストデータの分離
複数DataSource 複数のデータベースを使用するテスト
Spring Boot統合 Spring Boot環境でのテスト

サンプルコードはexamplesディレクトリを参照してください。

アーキテクチャ

DB Testerは、公開APIと内部実装を明確に分離した設計を採用しています。

モジュール 説明
db-tester-api 公開API(アノテーション、設定)
db-tester-core 内部実装(JDBC操作、CSVパーサー)
db-tester-junit JUnit Jupiter拡張
db-tester-spock Spock拡張
db-tester-*-spring-boot-starter Spring Boot統合
db-tester-bom Bill of Materials

JPMSを使用して、内部実装パッケージへのアクセスを制限しています。

動作要件

要件 バージョン
Java 21以上
JUnit 6(JUnit統合の場合)
Spock 2 + Groovy 5(Spock統合の場合)
Spring Boot 4(Spring Boot統合の場合)

まとめ

DB Testerは、以下の利点を提供します。

  • ボイラープレートコードの削減 - アノテーションによる宣言的なテスト記述
  • 直感的なテスト定義 - CSVファイルによるテストデータの可視化
  • 柔軟なデータ管理 - シナリオフィルタリングによるテストデータの共有
  • 外部依存なし - Pure JDBC実装で外部テストフレームワークへの依存がない
  • フレームワーク対応 - JUnit、Spock、Spring Bootとの統合

データベーステストの効率化に興味がある方は、ぜひ試してみてください。

関連リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?