@DataJpaTestや@SpringBootTestでテストデータを用意するたびにJdbcTemplateでINSERT文を手書きしていると、テストが増えるほど保守がつらくなってきますよね。
そこで便利なのが @Sql アノテーションです。Spring TestContext Frameworkの機能で、テスト実行の前後に指定したSQLスクリプトを宣言的に実行でき、テストデータをSQLファイルに分離して見通しや再利用性を上げられます。
メソッドレベルでの基本構文
テストメソッドに @Sql を付けるのが基本の使い方です。
@Test
@Sql("/test-data/users.sql")
void 登録済みユーザーを取得できる() {
var users = userRepository.findAll();
assertThat(users).hasSize(2);
}
先頭に / を付けるとクラスパスルートからの絶対パス、省略するとテストクラスのパッケージからの相対パスになります。複数スクリプトは scripts = {"schema.sql", "users.sql"} のように配列で指定でき、記述順に実行されます。
クラスレベルでの@Sql指定と使い分け
クラスに @Sql を付けると全テストメソッドに同じスクリプトが適用されます。メソッドレベルとクラスレベルを両方指定すると、デフォルトではメソッドレベルがクラスレベルを上書き(OVERRIDE)し、クラス側は実行されません。両方実行したい場合は @SqlMergeMode(MergeMode.MERGE) をクラスに付けます。
@DataJpaTest
@Sql("/test-data/common-categories.sql")
@SqlMergeMode(SqlMergeMode.MergeMode.MERGE)
class ProductRepositoryTest {
@Test
@Sql("/test-data/products.sql")
void 価格帯で商品を検索できる() {
assertThat(productRepository.findByPriceRange(1000, 3000)).hasSize(2);
}
}
付け忘れると共通データが投入されず失敗します。
executionPhaseでBEFORE/AFTERを使い分ける
executionPhase属性で「テスト前」「テスト後」どちらで実行するか指定できます(デフォルトはBEFORE_TEST_METHOD)。後片付け用スクリプトは@SqlGroupでBEFOREとAFTERをまとめて指定します。
@SqlGroup({
@Sql("/test-data/setup-orders.sql"),
@Sql(scripts = "/test-data/cleanup-orders.sql",
executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
})
@Test
void 注文一覧を取得できる() { ... }
@DataJpaTestのように自動ロールバックされる環境ではAFTER側はほぼ不要ですが、ロールバックされない構成(RANDOM_PORTなど)では重要になります。
@SqlConfigでerrorModeを制御する
DROP TABLEのような文はテーブル未存在でエラーになり得ます。@SqlConfigのerrorMode属性で制御しましょう。
@Sql(
scripts = "/test-data/reset-and-seed.sql",
config = @SqlConfig(errorMode = SqlConfig.ErrorMode.IGNORE_FAILED_DROPS)
)
@Test
void データをリセットしてから検証する() { ... }
IGNORE_FAILED_DROPSはDROP失敗のみ、CONTINUE_ON_ERRORは全エラーを無視し、デフォルトのFAIL_ON_ERRORは最初のエラーで止まります。
@DataJpaTestと組み合わせる
Repositoryのスライステストでは @DataJpaTest がよく使われ、デフォルトでトランザクションがロールバックされるので、さきほどの例のように@SqlはBEFORE_TEST_METHODだけで十分なことが多いです。
組み込みH2では、本番がMySQL/PostgreSQLだと方言差で構文エラーになることがあり、互換モード(MODE=MySQL)で回避できる場合があります。基本はRepositoryスライステストガイドで解説しています。
@SpringBootTest(RANDOM_PORT)と組み合わせる
TestRestTemplateを使ったE2E寄りのテストでは RANDOM_PORT を使いますが、トランザクションが自動ロールバックされない点に注意が必要です。
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Sql("/test-data/users.sql")
class UserApiTest {
@Test
void ユーザー一覧APIが取得できる() {
var response = restTemplate.getForEntity("/api/users", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
ロールバックされないので、AFTER_TEST_METHODでクリーンアップスクリプトを足すと後続テストへの影響を防げます。基本はTestRestTemplateによるE2Eテストガイドで扱っています。
data.sqlとの違いと使い分け
data.sqlは起動時に一度だけ実行される初期データ投入用、@Sqlはテスト単位で繰り返し実行されます。判断基準は次の3点です。
- 実行頻度: data.sqlは起動時に1回、@Sqlはテストごとに毎回
- データの性質: data.sqlは本番相当の初期マスタ向き、@Sqlは使い捨てフィクスチャ向き
- 保守責任: data.sqlの変更は全環境に影響、@Sqlはそのテスト内で完結
テストのためだけにdata.sqlを書き換えるのは避け、テスト固有のデータは@Sqlに切り出しましょう。
よくあるハマりどころ
- スクリプトパスの誤り: 先頭の
/の有無を間違えて相対パス扱いになり、ファイルが見つからず失敗する - 一意制約違反によるデータ競合: 同じIDを複数テストで使い回し、ロールバックされない環境で一意制約違反が起きる
- ロールバックされず後続テストに影響: AFTER_TEST_METHODのクリーンアップ漏れでデータが残り、後続テストが原因不明に失敗する
まとめ
@Sqlを使えば、手書きINSERTから離れてテストデータの投入を宣言的に書けます。クラス/メソッドの上書き関係とexecutionPhase・errorModeを押さえれば、@DataJpaTestや@SpringBootTest(RANDOM_PORT)どちらでも安心です。テストの書き方を見直すならJUnitとMockitoによるテストガイドも参考にしてみてください。