🔍

RSpecのテストケースごとにデータがリセットされる仕組みを調べてみた

2023/06/12に公開

背景

今までRailsのテストを書く際には、RSpecを利用していました。今まで当たり前のように利用していましたが、テストケースごとに、作成したデータがリセットされる仕組みを知りたくなったため、調べてみました。

結論

RailsでRSpecを実行したときには、テストケースごとにトランザクション内でデータを作成するようにしていました。テストケースが終了すると、トランザクションが終了したことになり、ロールバックが起きることで、DBへ作成したデータをリセットしていました。

調べたこと

# 動作環境
Ruby: 3.2.2
Rails: 7.0.5
Rspec: 3.12.0

そもそもの話、私は、RSpec自体がテストケースごとにデータをリセットする処理をしてくれていると思いこんでいましたが、これは間違いでした。
正しくはRailsがテストケースごとにデータをリセットする処理をしてくれています。その解説はactive_record/fixturesで書かれています。
https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/fixtures.rb#L165-L168

どのように制御しているの?

こんなテストコードがあるとします。

Rspec.describe User do

describe '#full_name' do
  let(:full_name_user) { create(:user, family_name: 'hoge', first_name: 'rails') }
  
  it do 
    expect(User.first.name).to eq 'hoge rails'
    # ここにbinding.bを追記する
  end
end

describe '#name' do
  before do
    create(:user, name: 'ruby')
  end
  
  it do 
    expect(User.first.name).to eq 'ruby'
  end
end

どのように実装されていることで、テストケースごとにデータをリセットできているのか知りたかったため、コードを追ってみました。実行されるコードを順番に追跡したいため、debug.gemを活用しました。
https://github.com/ruby/debug

試しにdescribe '#full_name'のendの直前にブレイクポイントを設置して、RSpecでテストを実行した時、#full_nameのテストで作成された変数full_name_userは、#nameのテストが実行された段階では、すでに削除されています。
そうすると、describe '#full_name'のendを抜けたあとに、実行されるコードで止める必要がありそうです。
どんどんコードを追跡していきます。describe '#full_name'のitのendが終わると、after_teardownメソッドが呼ばれます。
※tear downは取り壊す、破壊するの意味です。

https://github.com/rspec/rspec-rails/blob/v6.0.3/lib/rspec/rails/adapters.rb#L73-L76

いくつかのメソッドを経由して、ActiveRecord内に定義されているafter_teardownメソッドが呼ばれます。
https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/test_fixtures.rb#L5-L17

このteardown_fixturesメソッド内で、テストケースが終了後にロールバックする処理を担っています。
https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/test_fixtures.rb#L172-L187

順番にコードを見ていきましょう。
今回はrun_in_transaction?がtrueであるため、if文の中に入ります。
@fixture_connectionsには、Railsアプリが持つすべてのテーブル名やカラムの型情報やテスト内で作成されたレコードの数等が記録されています。
transaction_open?の定義元はこんな感じです。
https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L335-L337
今回は、transaction_open?の結果はtrueなので、connection.rollback_transactionメソッドが実行されます。
https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L308-L314
transaction.stateの中には、テストケースで作成したレコードの数ぶんの要素が入っています。

> transaction.state
#<ActiveRecord::ConnectionAdapters::TransactionState:0x00000001176fe100
 @children=
  [#<ActiveRecord::ConnectionAdapters::TransactionState:0x0000000117476888 @children=nil, @state=:committed>],
 @state=nil>

transaction.state.invalidated?はfalseなので、transaction.rollbackが実行されます。すると、DBに登録していたデータがリセットされます。
この流れをテストケースごとに繰り返すため、RSpecで実行されたテストは毎回、データがリセットされています。

RSpecでテストデータをリセットしたくない場合

この項目を設定すると、RSpecのテストケースごとにデータがリセットされるようになっています。

config.use_transactional_fixtures = true

私が関わっているプロダクトだと、rails_helper.rbに書かれていました。
rails_helper.rbには、RailsでRSpecを利用する際に、設定値を書いているファイルです。

Tips

もともと、Railsでは、use_transactional_testsuse_transactional_fixturesという設定項目名だったが、Rails5あたりで名前が変更されています。

変更されたPR: https://github.com/rails/rails/pull/19282
https://github.com/rails/rails/blob/18acbe8948e04088a3116ed3eb5df05ce72f0b20/guides/source/5_0_release_notes.md?plain=1#L668
Railsでは、use_transactional_testsの値はデフォルトでtrueになっています。
https://github.com/rails/rails/blob/18acbe8948e04088a3116ed3eb5df05ce72f0b20/activerecord/lib/active_record/test_fixtures.rb#L23
なら、RSpecでもuse_transactional_testsへ切り替えていると思いきや、use_transactional_fixturesという名前を使い続けていたりします。

https://github.com/rspec/rspec-rails/blob/main/lib/rspec/rails/fixture_support.rb#L30

最後に

今まで当たり前のように利用していたRailsとRSpecについて、少しだけ知見を深められたかなと思います。
調査に時間がかかってしまったため、もう少しスムーズにRailsやRSpecの定義元やコードの追跡をできるようになりたいです!

Discussion