RSpecのテストケースごとにデータがリセットされる仕組みを調べてみた
背景
今までRailsのテストを書く際には、RSpecを利用していました。今まで当たり前のように利用していましたが、テストケースごとに、作成したデータがリセットされる仕組みを知りたくなったため、調べてみました。
結論
RailsでRSpecを実行したときには、テストケースごとにトランザクション内でデータを作成するようにしていました。テストケースが終了すると、トランザクションが終了したことになり、ロールバックが起きることで、DBへ作成したデータをリセットしていました。
調べたこと
# 動作環境
Ruby: 3.2.2
Rails: 7.0.5
Rspec: 3.12.0
そもそもの話、私は、RSpec自体がテストケースごとにデータをリセットする処理をしてくれていると思いこんでいましたが、これは間違いでした。
正しくはRailsがテストケースごとにデータをリセットする処理をしてくれています。その解説はactive_record/fixturesで書かれています。
どのように制御しているの?
こんなテストコードがあるとします。
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を活用しました。
試しにdescribe '#full_name'のendの直前にブレイクポイントを設置して、RSpecでテストを実行した時、#full_nameのテストで作成された変数full_name_userは、#nameのテストが実行された段階では、すでに削除されています。
そうすると、describe '#full_name'のendを抜けたあとに、実行されるコードで止める必要がありそうです。
どんどんコードを追跡していきます。describe '#full_name'のitのendが終わると、after_teardownメソッドが呼ばれます。
※tear downは取り壊す、破壊するの意味です。
いくつかのメソッドを経由して、ActiveRecord内に定義されているafter_teardownメソッドが呼ばれます。
↓
このteardown_fixturesメソッド内で、テストケースが終了後にロールバックする処理を担っています。
順番にコードを見ていきましょう。
今回はrun_in_transaction?
がtrueであるため、if文の中に入ります。
@fixture_connections
には、Railsアプリが持つすべてのテーブル名やカラムの型情報やテスト内で作成されたレコードの数等が記録されています。
transaction_open?
の定義元はこんな感じです。
今回は、transaction_open?
の結果はtrueなので、connection.rollback_transaction
メソッドが実行されます。
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_tests
はuse_transactional_fixtures
という設定項目名だったが、Rails5あたりで名前が変更されています。
変更されたPR: https://github.com/rails/rails/pull/19282
Railsでは、use_transactional_testsの値はデフォルトでtrueになっています。
なら、RSpecでもuse_transactional_tests
へ切り替えていると思いきや、use_transactional_fixtures
という名前を使い続けていたりします。
最後に
今まで当たり前のように利用していたRailsとRSpecについて、少しだけ知見を深められたかなと思います。
調査に時間がかかってしまったため、もう少しスムーズにRailsやRSpecの定義元やコードの追跡をできるようになりたいです!
Discussion