遅延評価とRSpecとActive Record
仕事をしていて面白いコードを見たので紹介する。
次のコードについて話す。適当に書いているし、適宜コードも書き換えているので、間違っていたら適当に補完して読んで欲しい。
# app/models/user.rb class User < ApplicationRecord scope :admin, -> { where(role: :admin) } end
# spec/models/user_spec.rb RSpec.describe User do describe '.admin' do let(:user1) { FactoryBot.create :user, :as_admin } let(:user2) { FactoryBot.create :user } let(:user3) { FactoryBot.create :user } before do user2.update!(role: :admin) user3.update!(role: :admin) end it do expect(User.admin).to contain_exactly(user1, user2, user3) end end end
うごきそう
なにも考えずに見るとこのテストは動きそうに見える。user1
は最初からadminで、user2
とuser3
はbefore
の中でadminに変更されている。
つまりUser.admin
の結果はuser1
, user2
, user3
の3つのはずである。
確かにテストを実行すると成功する。
うごかなさそう
ところが、私は最初「なんでこのテストが通るのか」がさっぱり分からなかった。絶対に失敗すると思った。ところがテストは通っている。
RSpecのlet
は遅延評価である。つまりlet
で定義した名前が実際に参照されるまで、let
に渡したブロックは実行されない。
なにが言いたいかというと、User.admin
を実行するタイミングではまだuser1
は参照されていないので、user1
に対応するレコードは生成されていない。1
つまりUser.admin
はuser2
とuser3
のみを返すはずである。
次のようなコードを書くことで評価順を試すことができる。
def expect(_) Object.new.tap do |o| def o.to(_) end end end def contain_exactly(*_) end # 実行すると、1, 2の順で表示されるので、テストでは`User.admin`が`user1`よりも先に評価されていることがはっきりする。 expect(p 1).to contain_exactly(p 2)
実際にit
のブロックの先頭でbinding.pry
を仕込んでUser.admin
の返す値を見てみると、user2
とuser3
のみが含まれている。
it do # ここにbinding.pryを挿入。 # User.admin.count をすると、2が返る。 binding.pry expect(User.admin).to contain_exactly(user1, user2, user3) end
しかし、expect(User.admin).to contain_exactly(user1, user2, user3)
は成功する。なんだこれ。
でもうごく
ネタばれがあるのでこの問題の答えを自分で考えたい人はちょっと立ち止まってみよう
以下ネタばれ↓↓↓2
User.admin
が返すのはActiveRecord::Relation
である。そして、ActiveRecord::Relation
は評価されただけではSQLを発行しない。
そのため、user1
が評価された後にSQLが発行されるようになっている。
次のような実行順序でコードが評価される。
expect
メソッドの引数のUser.admin
の評価expect
メソッドの評価to
メソッドの引数のcontain_exactly
メソッドの引数のuser1
,user2
,user3
の評価contain_exactly
メソッドの評価to
メソッドの評価- 多分ここでSQLが発行される
結論
難しいからlet!
を使うとかbefore
でuser1
を呼び出したりとかした方が良さそう。