1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

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

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

letの書き方

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

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

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

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

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

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

(以下の内容は、@junchitoさんの記事を参考にさせていただいています。書籍も含めて、本当にいつもお世話になっています)
参考:RSpecのletを使うのはどんなときか?(翻訳)

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

(1) typoにすぐ気づくことができる。

* typoとは、「誤植」を意味するtypographical errorの略。簡単に言えばタイプミスのこと。

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

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

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

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

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

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

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があったとします。

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」を実行すると、以下のようなエラーが出ます。

ターミナル
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を用いてみましょう。

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」を実行すると……

ターミナル
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を用いてテストデータを作成します。冒頭でも紹介しましたが、以下のようにします。

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

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

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

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

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に置き換えることができます。

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

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に置き換えてみます。

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を使った方が、テストが読みやすくなる」 と、伊藤淳一さんもおっしゃっています。
参考:RSpecのletを使うのはどんなときか?(翻訳)

参考にさせていただいた記事

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」

RSpecのletを使うのはどんなときか?(翻訳)

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

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

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?