【RSpec】letを使用するメリット

　最近、RSpecによるテストにおいて、インスタンス変数の代わりに「let」という機能を用いることができると知りました。

　そこで、自分もletを使ってみようと考えたのですが、
「letを使うとどんなメリットがあるの？」
という疑問が湧いたので、備忘録としてまとめることにしました。

　今回、参考にさせていただいた記事を一番下にまとめていますので、気になる方はそちらをご覧ください。

　同じような疑問を持っている方の参考になると幸いです。

# letの書き方
　先ほども書きましたが、テストにおいて、インスタンス変数を「let」に置き換えることができます。

今回はFactoryBotを使用している場合の書き方を紹介します。

```ruby
#インスタンス変数を使用した場合
before do
  @インスタンス変数名 = FactoryBot.create(:モデル名)
end

#↓以下のように書ける！

#letを使用した場合
let(:メソッド名) { FactoryBot.create(:モデル名) }
```

# letを使用するメリットとは？

（以下の内容は、@junchitoさんの記事を参考にさせていただいています。書籍も含めて、本当にいつもお世話になっています）
参考：<a href="https://qiita.com/jnchito/items/cdd9eef2ed193267c651">RSpecのletを使うのはどんなときか？（翻訳）</a>

まず、letを使用するメリットについてですが、

#### (1) typoにすぐ気づくことができる。
＊ typoとは、「誤植」を意味するtypographical errorの略。簡単に言えばタイプミスのこと。

　「ユーザー登録に関するテスト」を例にして説明したいと思います。

以下は、Userモデルのファイルです。

```ruby:app/models/user.rb
class User < ApplicationRecord
  # ユーザーのニックネームに関するバリデーション
  validates :nickname, presence: true
end
```

以下はUserモデルのテストデータを生成するFactoryBotのファイルです。

```ruby:spec/factories/users.rb
#ユーザー登録のテストデータを生成
FactoryBot.define do
  factory :user do
    nickname { "Taro Tanaka" }
  end
end
```

以下がUserモデルの単体テストコードです。

```ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーの新規登録' do
    before do
      @user = FactoryBot.build(:user)
    end
    context '新規登録できない場合' do
      it 'nicknameが空では登録できないこと' do
        @user.nickname = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("Nickname can't be blank")
      end
    end
  end
end
```

さて、ここでUserモデルの単体テストコードに、次のようなtypoがあったとします。

```ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーの新規登録' do
    before do
      @user = FactoryBot.build(:user)
    end
    context '新規登録できない場合' do
      it 'nicknameが空では登録できないこと' do

        # 「@user」のはずが、rが抜けて「@use」に間違えている
        @use.nickname = nil

        @user.valid?
        expect(@user.errors.full_messages).to include("Nickname can't be blank")
      end
    end
  end
end
```

このままターミナルで「bundle exec rspec」を実行すると、以下のようなエラーが出ます。

```none:ターミナル
Failures:

  1) User ユーザーの新規登録 新規登録できない場合 nicknameが空では登録できないこと
     Failure/Error: @use.nickname = nil
     
     NoMethodError:
       undefined method `nickname=' for nil:NilClass
     # ./spec/models/user_spec.rb:15:in `block (4 levels) in <main>'
```

「Failure/Error: @use.nickname = nil」の行を注意して見ればtypoに気づくことができるかもしれませんが、そのまま見逃して
「あれ！？　なんでNoMethodErrorが出たんだろう！？」
となりそうですよね。

そこで、インスタンス変数の代わりにletを用いてみましょう。

```ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーの新規登録' do

   # インスタンス変数の代わりにletを使用
   let(:user) { FactoryBot.build(:user) }

    context '新規登録できない場合' do
      it 'nicknameが空では登録できないこと' do

        # 「user」のはずが、rが抜けて「use」に間違えている
        use.nickname = nil

        user.valid?
        expect(user.errors.full_messages).to include("Nickname can't be blank")
      end
    end
  end
end
```

この状態でターミナルで「bundle exec rspec」を実行すると……

```none:ターミナル
Failures:

  1) User ユーザーの新規登録 新規登録できない場合 nicknameが空では登録できないこと
     Failure/Error: use.nickname = nil
     
     NameError:
       undefined local variable or method `use' for #<RSpec::ExampleGroups::User::Nested::Nested_2:0x00007fbbec3ed7f8>
       Did you mean?  user
     # ./spec/models/user_spec.rb:15:in `block (4 levels) in <top (required)>'
```

　このように、
「'use'というメソッドは定義されていないですよ。正しくは'user'ではないですか？」
とNameErrorで教えてくれるわけですね。

 これがletを使用する1つ目のメリット、「typoに気付きやすくなる」でした。

#### (2) 無駄な初期化の時間を無くすことができる
インスタンス変数を用いてテストを行う場合、beforeを用いてテストデータを作成します。冒頭でも紹介しましたが、以下のようにします。

```ruby
#インスタンス変数を使用した場合
before do
  @インスタンス変数名 = FactoryBot.create(:モデル名)
end
```

このように、beforeを使用した場合、beforeは各example（各it '○○' do 〜 endのこと）の前に実行されます。

その際、あるexampleで使われないインスタンス変数があっても、before内に定義されていれば、その不必要な変数も一緒に生成されてしまいます。
つまり、余計な時間がかかってしまうわけですね。

 先ほどのUserモデルで例を出します。以下のように、before内に「@image」というインスタンス変数を作ったとしましょう。

```ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'ユーザーの新規登録' do

   before do
    @user = FactoryBot.build(:review)
    @image = fixture_file_upload('public/images/test.jpg')
  end

    context '新規登録できない場合' do
      #必要なインスタンス変数は@nicknameのみ
      it 'nicknameが空では登録できないこと' do
        @user.nickname = nil
        @user.valid?
        expect(@user.errors.full_messages).to include("Nickname can't be blank")
      end
    end

      it 'imageが空では登録できないこと' do
　　　　 #必要なインスタンス変数は@imageのみ
        @image = nil
        @image.valid?
        expect(@user.errors.full_messages).to include("Image can't be blank")
      end
  end
end
```

この時、
「'nicknameが空では登録できないこと'のテストに必要なインスタンス変数は@userのみ、@imageはいらない」
「'imageが空では登録できないこと'のテストに必要なインスタンス変数は@imageのみ、@userはいらない」
となっています。
しかし、それに関わらず、どのexampleでも、@userと@imageの両方が生成されてしまうわけです。

今回のような例なら大した問題になりませんが、これが
「exampleが何百とある」
「インスタンス変数を何個も作っている」
というような大規模なテストだった場合、「使わないインスタンスを生成する」という行為を何百回と繰り返すわけですから、無駄な時間がかかってしまうわけですね。

そこで、インスタンス変数の代わりにletを使えば、

「'nicknameが空では登録できないこと'をテストするのに必要な'userメソッド'のみを生成する」

「'imageが空では登録できないこと'をテストするのに必要な'imageメソッド'のみを生成する」

といったことが働きをしてくれるので、「不要なインスタンス変数を作成する」という無駄な時間を削ることができます。このように、letで定義されたメソッドを使うと、そのメソッドを呼び出した時のみ初期化処理を実行してくれます。

以上、2つ目のメリット「無駄な初期化の時間を無くすことができる」でした。

#### (3) ローカル変数をそのままletに置き換えることができる
　ここでのローカル変数とは、example内(it '○○' do 〜 end)で定義した変数のことです。ローカル変数は、そのexample内でしか使うことができず、他のexampleでは使うことができません。

　このローカル変数のシンタックス（構文、書き方）を変更することなく、そのままletに置き換えることができます。

例えば、「重複したニックネームを保存することはできない（他のユーザーと同じニックネームは使えない）」というバリデーションを設定した上で、そのことをインスタンス変数を使ってテストします。

```ruby:spec/models/user_spec.rb
before do
    @user = FactoryBot.build(:review)
end

it '重複したnicknameが存在する場合登録できないこと' do
    @user.save
    another_user = FactoryBot.build(:user, nickname: @user.nickname)
    another_user.valid?
    expect(another_user.errors.full_messages).to include('Nickname has already been taken')
end
```

この「another_user」がローカル変数に該当しますね。
それでは、インスタンス変数をletに置き換えてみます。

```ruby:spec/models/user_spec.rb
let(:user) { FactoryBot.build(:user) }

#（他のexampleは省略）

it '重複したnicknameが存在する場合登録できないこと' do
    user.save
    another_user = FactoryBot.build(:user, nickname: user.nickname)
    another_user.valid?
    expect(another_user.errors.full_messages).to include('Nickname has already been taken')
end
```

このように、example内では、@userの@を削除するだけで済みました。

以上、3つ目のメリット、「ローカル変数をそのままletに置き換えることができる」でした。

#### (4) テストが読みやすくなる

　主観的な話になるみたいですが、「letを使った方が、テストが読みやすくなる」 と、伊藤淳一さんもおっしゃっています。
参考：<a href="https://qiita.com/jnchito/items/cdd9eef2ed193267c651">RSpecのletを使うのはどんなときか？（翻訳）</a>

# 参考にさせていただいた記事
<a href="https://qiita.com/jnchito/items/42193d066bd61c740612">使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」</a>

<a href="https://qiita.com/jnchito/items/cdd9eef2ed193267c651">RSpecのletを使うのはどんなときか？（翻訳）</a>

　いかがでしたか？
　僕自身、RSpecを本格的に学んで間もないため、至らない説明があるかもしれません。
　ご指摘などありましたら、教えていただけますと幸いです。

ここまで読んでいただき、ありがとうございました！