🥝

Database RiderでのCSVテストデータの外部キー制約エラー: 原因と解決策

2024/05/07に公開

背景

私たちはSpring Bootを用いたサービス開発をしており、その統合テストでDatabase Riderを活用してCSV形式のテストデータをDBにインポートしています。テストデータの管理には、アプリケーション用の権限が制限されたDBユーザーと、テストデータの管理に幅広い権限を持つDBユーザーの二種類を使用しています。詳細なセットアップについては以前の記事で触れています:

https://zenn.dev/nstock/articles/845a2954d8a3e4

今回の投稿では、このセットアップを基に進める中で発生した、テストデータのインポートエラーとその解決策について詳述します。

課題:外部キー制約のあるテーブルにテストデータをインポートする際のエラー

統合テストのケースが増えるにつれて、テストデータのインポート時に以下のようなエラーが頻発しました。

テスト実行時のエラーメッセージ
ERROR:  update or delete on table "product" violates foreign key constraint "fk_product" on...DETAIL:  Key is still referenced from table "product_metadata".

このエラーは、productテーブルを更新または削除しようとした際に、product_metadataテーブルが外部キー制約でproductテーブルを参照しているため、操作を完了できないことを示しています。

原因: テストデータセットのtable-ordering.txtの内容に不整合あり

原因は、@DataSetで指定しているテストデータセットの、table-ordering.txtの対象テーブルがテスト毎にばらつきがあったことでした。

これは、@DataSetアノテーションが何をしているかに関係しています。

@DataSetアノテーションでcsvファイルを利用してテストデータを流し込む際、table-ordering.txtというファイルをテストデータと同じ階層においておくと、cleanBeforeオプションがその順番にテーブルのデータを消し、その後テストデータを流し込んでくれます。

しかし、このtable-ordering.txtに含まれるテーブルの一覧が、他のテストデータのtable-ordering.txtと内容が違うと、削除するテーブルにも差分が出ることがわかりました。

例えば、 testdata-1testdata-2 の二つのテストデータセットが以下のように異なる場合、エラーが発生します。

// testdata-1/table-ordering.txt
user
product
product_metadata

// testdata-2/table-ordering.txt
user
product

product_metadataはproductテーブルへの外部キー制約が存在するため、testdata-1を使用後にtestdata-2を使用すると、外部キー制約によるエラーが発生します。

解決策

解決策として、table-ordering.txtの内容に左右されずに、テストごとにDBの内容を一貫して初期化することが有効です。

採用している解決策: 初期化用のデータセットをテスト前にインポート

DBRiderを用いてデータ初期化を実施する方法をチームメンバーのJagaさんがひらめき、現在はこちらの解決策を採用しています。

その方法は、初期化用のテストデータセットをテスト実行前に流し込む、というものです。

具体的には、以下のようなテストデータセットを用意します。

  • ヘッダのみでデータは含まない、全テーブルのテストデータCSVファイル群
  • 全てのテーブル名が記載されたtable-ordering.txt

このテストデータセットを、@BeforeEachにて流し込むことでDBを初期化させます。
このデータセットを流し込む手順では、以下のようなことが起こっています。

  1. table-ordering.txtに全てのテーブル名が記載されているので、最初に全てのテーブルに対してDELETEが走ります。
  2. その後、全てのテーブルのデータを、CSVファイルをもとに流し込もうとします。ただし、CSVファイルのレコード内容は空なので、特にレコードが追加されることはありません。

以下のようなコード例で実現しています。

@SpringBootTest
@DBRider(name = "dbRiderDataSource")
public DummyApiTest {

    @BeforeEach
    @DataSet(value = "empty/user.csv", cleanBefore = true)
    public void beforeEach() {
    }

    @Test
    @DataSet(value = "testdata-1/user.csv")
    void test_ユーザー一覧が表示できること() {
    }

    ...
}

トライしたが採用していない解決策:@Transactionalアノテーション

統合テスト時のDBの初期化には@Transactionalアノテーションが利用できることを、プロになるためのSpring入門で知りました。
以下の記事を参考に、DBテーブルの初期化に @Transactional アノテーションを利用する方法を試しました。

Database RiderをSpring Framework(Spring Boot)と合わせて使う時に、Rider JUnit 5とRider Springのどちらを使うか? - CLOVER🍀

dbrider-junit5 ライブラリを利用していると@Transactionalアノテーションがうまく動作せずdbrider-spring ライブラリを利用するとよい点が大変参考になりました。

しかし、背景で記載した、複数のDBユーザーを用いる私達の設定の場合、@Transactionalアノテーションは想定どおり機能しないことがわかり、この方法を取ることは断念しました。

まとめ

DBユーザーに権限制約がある状況下でDatabase Riderを利用する際に、CSVファイルからのデータインポート時に発生するエラーの解決法を書きました。

Nstockではエンジニアを募集しています!
👨‍👩‍👧‍👦カジュアル面談から気軽にお話しましょう🤞

https://speakerdeck.com/nstock/we-are-hiring
https://nstock.co.jp/recruit

Nstock Tech Blog

Discussion