はじめに
データベースを利用するアプリケーションのテストでは、テストデータの準備、テスト後の状態検証、複数テストケース間でのデータ分離など、考慮すべき事項が多くあります。
筆者自身、これらの作業を繰り返す中で「アノテーションを付けるだけでCSVからデータを投入し、結果を検証できれば便利なのでは」と考え、JUnitおよびSpockに対応したデータベーステストフレームワーク「DB Tester」を作成しました。本記事では、このライブラリの機能と使い方を紹介します。
- GitHub: https://github.com/seijikohara/db-tester
-
Maven Central:
特徴
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
testImplementation(platform("io.github.seijikohara:db-tester-bom:0.1.0"))
testImplementation("io.github.seijikohara:db-tester-junit")
Maven
<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. テストクラスの作成
@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]列でテストメソッドごとにデータを分離します。
[Scenario],ID,NAME,EMAIL
shouldCreateUser,1,existing,existing@example.com
shouldUpdateUser,1,target,target@example.com
[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 |
ファイルシステムの絶対パス |
| 空文字列 | "" |
規約ベースの自動検出(デフォルト) |
データベース操作
@Preparationのoperation属性で指定できる操作の一覧です。
| 操作 | 説明 | ユースケース |
|---|---|---|
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行目の
DESCRIPTIONはNULL - 2行目の
DESCRIPTIONは空文字列
テーブル処理順序
外部キー制約を考慮して、テーブルの処理順序を制御できます。
TableOrderingStrategy
@Preparationおよび@ExpectationのtableOrdering属性で順序決定の戦略を指定できます。
| 戦略 | 説明 |
|---|---|
AUTO |
自動決定(デフォルト) |
LOAD_ORDER_FILE |
load-order.txtを使用(必須) |
FOREIGN_KEY |
JDBCメタデータで外部キー依存関係を解決 |
ALPHABETICAL |
テーブル名でアルファベット順にソート |
AUTO戦略では以下の優先順位で順序を決定します:
-
load-order.txtが存在する場合はそれを使用 - JDBCメタデータから外部キー依存関係を解決
- アルファベット順にフォールバック
外部キー自動解決
DB Testerはデータベースメタデータから外部キー関係を自動的に取得し、テーブルの処理順序を決定します。親テーブル(参照される側)が子テーブル(外部キーを持つ側)より先に処理されるため、外部キー制約違反を防ぐことができます。
この機能はJDBCのDatabaseMetaData.getExportedKeys()を使用して実装されており、特別な設定は不要です。
循環参照が検出された場合は警告をログに出力し、データセット宣言順が維持されます。
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プロパティを提供する必要があります。
@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との統合
データベーステストの効率化に興味がある方は、ぜひ試してみてください。