11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RailsのMinitestでテスト実行後に、データが残ってしまう問題(複数DBを使用)

Last updated at Posted at 2023-03-27

RailsのMinitestを使用していて、github actionを使用してCIを導入しようとしているのですが、その際に苦戦した話と対応方法を記載しておこうと思います。

前提

・1つのデータベースではなく、複数のデータベースを取り扱っている
・データベース単位かつテスト単位でfixtureを設定している
・setupの中でfixtureを設定している

テスト実行後にDBに値が残り、それが他の自動テストのアサートに影響を与えてしまう

minitestを使用している時、基本的にはsetupの中でfixtureを設定しているようにしているのですが、テスト内で異なるDB同士でテーブルをjoinしているケースの場合にトランザクション範囲の関係で、fixtureで設定したデータが主テーブルに関しては引けるが、子テーブルでは引けないという事態になっていました。それを避ける為、runメソッドやuse_transactional_testsなどのオプションを使用することでトランザクションを無効にする対応を取っていました。ただこうしたケースの場合、transactionが効かない状態なのでロールバックされず、テスト実行後もデータが残り続けてしまいます。そしてデータが残ってしまうと、その他のテストでレコードが存在しないはずなのに取れてしまい、アサートがズレてしまうという現象が起きるわけです。

対策方法(teardownを活用しよう)

考えた対策として、teardownの中でデータ削除の処理を行うようにすることにしました。teardownは各テスト実行後に呼ばれるメソッドで、タイミングとしてここに記載するのが適切だと考えました。

customer_controller_test.rb
setup do
    ActiveRecord::FixtureSet.reset_cache
    fixture_directory = Rails.root.join('test', 'fixtures', 'customer_controller_test')
   
    fixture_files = [
      :customer_products
    ]
    @fixture_models_customer_product = {
      customer_products: CustomerProducts
    }

    ActiveRecord::FixtureSet.create_fixtures(fixture_directory, fixture_files, @fixture_models_customer_product, CustomerProductsDb)
    
    fixture_files = [
      :customer_info
    ]
    @fixture_models_customer_info = {
      customer_info: CustomerInfo
    }
    ActiveRecord::FixtureSet.create_fixtures(fixture_directory, fixture_files, @fixture_models_customer_info, CustomerInfoDb)
  end

  teardown do
    delete_database
  end

teardownに記載しているdelete_databaseですが、これはtest_helperに定義しているメソッドです。ここではポイントとして、モデル情報を定義するハッシュ値をインスタンス変数にしている点です。こうすることでtest_helper内からもこの値を参照することができます。

test_helper.rb
  def delete_database
    if @fixture_models_customer_product.present?
      CustomerProductsDb.connection.execute "SET FOREIGN_KEY_CHECKS = 0;"
      @fixture_models_customer_product.values.each { |model| model.delete_all }
      CustomerProductsDb.connection.execute "SET FOREIGN_KEY_CHECKS = 1;"
    elsif @fixture_models_customer_info.present?
      CustomerInfoDb.connection.execute "SET FOREIGN_KEY_CHECKS = 0;"
      @fixture_models_customer_info.values.each { |model| model.delete_all }
      CustomerInfoDb.connection.execute "SET FOREIGN_KEY_CHECKS = 1;"
    end
  end

あらかじめDB単位でここのif分に記載しておけば、運用としてrunメソッドやuse_transactional_testsを用いているテストは、teardownでこの関数を呼びさえすれば勝手にデータを削除をしてくれるような仕組みにしています。データを削除する際に外部キー制約を貼っている場合、その子テーブルを先に削除しろと怒られるので、SET FOREIGN_KEY_CHECKS = 0;で一時的に外部キー制約を無効にし、データを削除した後その外部キー制約を元に戻すようにしています。先頭のconnection.executeはActiveRecord::Baseクラスが保持しているSQLの実行を行うメソッドで、connection.executeの後にクエリを記載することでそのクエリを実行してくれます。

終わりに

CIを導入するつもりが、思わぬ伏兵に出くわして本来やろうとしてたことから大きくその範囲が広がってしまった印象です。同じようなケースで悩んでいる方がいらっしゃれば参考にして頂ければと思います。

11
2
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
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?