最近、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モデルのファイルです。
class User < ApplicationRecord
# ユーザーのニックネームに関するバリデーション
validates :nickname, presence: true
end
以下はUserモデルのテストデータを生成するFactoryBotのファイルです。
#ユーザー登録のテストデータを生成
FactoryBot.define do
factory :user do
nickname { "Taro Tanaka" }
end
end
以下がUserモデルの単体テストコードです。
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があったとします。
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を用いてみましょう。
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」というインスタンス変数を作ったとしましょう。
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に置き換えることができます。
例えば、「重複したニックネームを保存することはできない(他のユーザーと同じニックネームは使えない)」というバリデーションを設定した上で、そのことをインスタンス変数を使ってテストします。
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に置き換えてみます。
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を本格的に学んで間もないため、至らない説明があるかもしれません。
ご指摘などありましたら、教えていただけますと幸いです。
ここまで読んでいただき、ありがとうございました!