Railsのテストで値が変わらないときはreloadを疑おう
RSpecでテストを書いていて、「データが更新されているはずなのに、期待した値になっていない」と感じることがありました。
その原因のひとつが ActiveRecordのキャッシュ です。
ここでは、ActiveRecord + RSpecのテストで、モデルの更新が正しく検証できない問題と、reload
メソッドによるその解決方法について解説します。
reload
を使わないとテストが失敗することがある
次のような処理を持つコントローラがあるとします。
class PlansController < ApplicationController
def destroy
@plan = Plan.find(params[:id])
@plan.soft_delete!
end
end
これに対し、RSpecのテストを以下のように書いていました。
describe "#destroy" do
let(:plan) { create(:plan) }
it "削除日時が設定される" do
delete :destroy, params: { id: plan.id }
expect(plan.deleted_at).to be_present
end
end
このテストは、deleted_at が nil のままで失敗することがあります。しかし、実際にはデータベース上では更新されています。なぜテスト内では変更が反映されていないのでしょうか。
解決:reload を使ってインスタンスを最新状態にする
このような問題を防ぐには、reload を使ってインスタンスをデータベースの最新の状態に更新します。
it "削除日時が設定されること" do
delete :destroy, params: { id: plan.id }
expect(plan.reload.deleted_at).to be_present
end
reload によって、キャッシュされていた情報が更新され、deleted_at の最新の値が正しく取得できます。
なぜ reload が必要なのか?
ActiveRecordのインスタンスは、一度作成されるとその状態をオブジェクト内にキャッシュします。言い換えるならば、一度DBから読み込んだ情報を、Rubyオブジェクトが自分の中に保持し続けるということです。
今回の場合↓
describe "#destroy" do
let(:plan) { create(:plan) }
it "削除日時が設定されること" do
delete :destroy, params: { id: plan.id }
expect(plan.deleted_at).to be_present # 古い状態(nil)を見ている
end
end
-
create(:plan)
によって、Planのインスタンスが作成される -
この時点で、
plan
という オブジェクトはdeleted_at: nil
の状態を持っている
→ これは当然の初期状態。 -
delete :destroy
を呼ぶと、コントローラ経由でDB上のレコードのdeleted_at
が現在時刻に更新される
→ データベースの内容は正しく更新されています。 -
しかし、
plan
オブジェクトは自分の中にあるdeleted_at: nil
の情報を保持し続ける
→ ここがポイント!Rubyのオブジェクトは、更新されたDBの状態を自動では反映しないため、plan
の属性は古いままです。これが ActiveRecordのキャッシュ です。 -
その結果、
expect(plan.deleted_at)
はキャッシュされた古い状態(nil
)を見てしまい、テストが失敗する
そのため、DB上で変更があっても、Rubyオブジェクトの属性はすぐには更新されないのです。
こちらの記事の「ActiveRecord Relationのプロパティとその役割」にも関連する内容が書かれているので、気になる方はぜひ参考にしてみてください。
まとめ
RSpecでActiveRecordで属性変更をテストする際は、reload を使って最新の状態にすることが大事です。特に、更新直後に属性をチェックするテストでは、reload を使うことで正確に検証することが可能になります。
参考文献
Discussion