letに対する疑問点
前提知識として、
・letは呼び出される度に遅延評価される
という挙動がある。
ということは、呼び出される度に作成されてしまうのか?
つまり、「同じletだとしても3回呼び出されたら3つとも違うオブジェクトが作られてしまうのか?」
という疑問や、
「違うexample同士でも, 一度作られてしまったオブジェクトは共有されてしまうのか?」
「同じモデルを複数作ることは可能なのか?」
といった疑問があったため、実際に試してみました。
いきなり結論
結論から言うと、
・ 同じexample内なら同じオブジェクトを返します
つまり、exampleを通過する度に、前回のオブジェクトはキャッシュされるということ。
と、言葉で言ってもわかりづらいので、実際に動かしながら説明していきますね。
まず、そもそもletって何?って方は伊藤さんの記事を参考にしてほしいです。めちゃくちゃわかりやすいので。
https://qiita.com/jnchito/items/42193d066bd61c740612
一般的なletの使い方
letは使う度に呼び出される.
頭の悪い僕は、同じexample内(it 〜 end)
でもlet
が呼び出されるたびに違うオブジェクトが作られてしまうと解釈していた。
例えば以下の例で言えば、
RSpec.describe User, type: :model do
let(:user) { create(:user) }
it "名前が一致する" do
expect(user.name).to eq user.name
end
end
# 1 example, 0 failures
# User.count => 1
順序としては以下のようになる。
⭕️
1. expectの次に、user
が作られる
2. 最初に呼び出されたuser
同士で名前が一致するか確かめる
しかし僕は、以下のように勘違いしていた。
❌
1. expectの次にuser
が作られる
2. eq
の次に、二つ目のuser
が作られる
3. 最初に呼び出されたuser
と2回目に呼び出されたuser
の名前を比較している
同じexample内で二回使っているが、何度呼び出されようが一度letから呼び出されたものは同じオブジェクトとして使いまわすことができるらしい。
すなわち、User.count
は2ではなく、1になる。
つまり、以下のように何回呼んでも同じオブジェクトが呼ばれる。
RSpec.describe User, type: :model do
let(:user) { create(:user) }
it "名前が一致する" do
user
user
user
expect(user.name).to eq user.name
end
end
# 1 example, 0 failures
# User.count => 1
もし確かめたい方は、binding.pry
を挟んで確かめてみましょう。
it "名前が一致する" do
user
binding.pry
expect(user.name).to eq user.name
end
userを何度呼んでも、同じexample内であればUser.count
は変わらないはずです。
同じモデルを違う変数名でletを呼び出す
変数名さえ違えば、同じモデルを複数作れるのかな?と思い、実験。
RSpec.describe User, type: :model do
let(:user) { create(:user, name: "taro") }
let(:user_2) { create(:user, name: "hanako") }
it "名前が一致する" do
expect(user.name).to_not eq user_2.name
end
end
# => 1 example, 0 failures
# User.count => 2
テストは無事通りました。
流れとしては以下の感じです。
1. expect
で、一つ目のUser(nameがtaro)が作られる.
2. eq
の次に、二つ目のUser(nameがhanako)が作られる.
このように、別々のオブジェクトとして作られます。
もしユニークなカラムがあってバリデーションに引っかかってしまうと
ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry
のようにエラーが発生してしまうので、意味のある変数名、意味のある値とするようにしましょう。
例)
let(adult_user) { creaet(:user, age: 20) }
let(child_user) { creaet(:user, age: 10) }
違うexampleから呼び出した場合
違うexampleから呼び出されたオブジェクトは別々のオブジェクトとして呼び出されるのか実験。
RSpec.describe User, type: :model do
let(:user) { create(:user) }
it "名前が一致する" do
expect(User.count).to eq 1
# User.count => 1
end
it "名前が一致する" do
expect(User.count).to eq 2
# User.count => 1
end
end
# # => 2 examples, 1 failure
Failure/Error: expect(User.count).to eq 2
expected: 2
got: 1
二つ目のexampleで落ちてしまう結果に。
処理の流れ
1. 一つ目のexampleでUserが作られる
2. 2つ目のexampleでは、一つ目のexampleで作られたUserは消され、クリーンな状態で新しくUserが作られる
ここで言う、一つ目のexampleで呼び出されたオブジェクトと、二つ目のexampleで呼び出されたオブジェクトは全く別物だということがわかりました。
つまり、example
が走るたびに、前回作られたオブジェクトはキャッシュされずに削除されるというわけです。
まとめ
・同じexample内で作られたオブジェクトはキャッシュされて残る
・変数を変えれば, 一つのexample内で同じモデルを呼ぶことができる
・exampleで作られたオブジェクトは2回目以降キャッシュされずに削除される
基本的にこの3点を押さえておけばある程度大丈夫だと思いますが、まだまだ初心者を悩ませる部分が多いと思うので随時更新していきます!