LoginSignup
7
5

More than 5 years have passed since last update.

【Rspec】ややこしいletの挙動についてまとめてみる。

Last updated at Posted at 2019-02-28

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点を押さえておけばある程度大丈夫だと思いますが、まだまだ初心者を悩ませる部分が多いと思うので随時更新していきます!

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5