はじめに
RSpecにはlet
という機能があります。
これを使うとインスタンス変数を次のように置き換えることができます。
# インスタンス変数を使う場合
before do
@user = User.new(name: 'Taro', email: 'taro@example.com')
end
it 'is valid' do
expect(@user).to be_valid
end
# letを使う場合
let(:user) { User.new(name: 'Taro', email: 'taro@example.com') }
it 'is valid' do
expect(user).to be_valid
end
RSpecを使い慣れている人であれば、おそらくlet
を使うことが多いと思いますが、初心者の人には違いやメリットがよくわからないと思います。
また、使い慣れている人であっても具体的な違いをぱっと即答できる人は少ないんじゃないでしょうか?
ネットを調べていたところ、Stack OverflowにRSpecの開発者であるMyron Marston氏の模範回答が載っていたので、本記事ではこの内容を翻訳してみます。
質問
私はbeforeブロックでインスタンス変数を定義し、それらを各exampleで使う、というコードをよく書いています。
しかし、最近私はlet()
という機能があることに気付きました。RSpecのドキュメントにはこのように書かれています。
letはメモ化のためのヘルパーメソッド(memoized helper method)を定義するために使います。
値はキャッシュされ、同じexampleの中では何度呼ばれても同じ値(同じオブジェクト)が返ります。
しかし、exampleが異なる場合は異なるオブジェクトが返ります。
私の疑問はbeforeブロックの中でインスタンス変数を定義することとどんな違いがあるのか、という点です。
また、みなさんはどういうときに let()
と before()
を使うようにしていますか?
Myron Marston氏の回答(letの利点)
私は以下のような理由からインスタンス変数ではなくlet
をいつも使っています。
1. typoにすぐ気づける
インスタンス変数は参照された瞬間に生成されます。
これが問題になるのはインスタンス変数の名前をtypoしたときです。
typoすると新しいインスタンス変数が生成されてnil
で初期化されます。
これはわかりにくいバグや、パスすべきでないテストがパスする問題(false positive)につながります。
let
はメソッドを作るのでtypoするとNameError
が発生します。
本来であればこちらの動きの方が望ましいでしょう。
また、このような特性(訳注:テストコードの問題をすぐ検知できること)を持つのでテストのリファクタリングもしやすくなります。
2. 無駄な初期化の時間を無くせる
before(:each)
フックは各exampleの前に実行されます。
その際、exampleで使われないインスタンス変数があっても、before(:each)
フックの中で定義されていればその変数が生成されます。
ほとんどのケースではこれは大した問題になりませんが、 もしインスタンス変数のセットアップに長い時間がかかるのであれば、毎回無駄な時間を食うことになります。
一方、 let
で定義されたメソッドを使うと、初期化処理が実行されるのはそのメソッドが呼ばれたときだけ です。
3. ローカル変数をそのままletに置き換えられる
exampleの中でローカル変数を使っている場合、 シンタックスを変更することなくそのままlet
を使ったコードにリファクタリングできます。
一方、 インスタンス変数を使うようにリファクタリングすると、参照方法を変更する必要が出てきます。 (例:@
を付ける)
4. letを使った方が読みやすい
これはちょっと主観的な話になりますが、Mike Lewis氏(訳注:別の回答者)も述べているように、 let
を使った方がテストが読みやすくなる と思います。
テストが依存するオブジェクトはすべてlet
を使って整理し、it
ブロックの中のコードは短くスッキリさせるのが私の好みです。
コメント欄での補足(もう少し詳しく知りたいというコメントに対して)
let
はテストが依存するオブジェクトを定義し、before(:each)
は必要な設定やモック/スタブのセットアップで使うのが便利だと思います。
一つの巨大なbeforeフックで全部のことをやろうとするより、こんなふうに分ける方が私は好きです。
また、before(:each) { @foo = Foo.new }
よりもlet(:foo) { Foo.new }
と書いた方がスッキリしますし、意味もわかりやすいと思います。
具体的なコード例はこちらを見てください。
# https://github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/hooks_spec.rb#L9-16
subject { HooksClass.new }
# letではテストが依存するオブジェクトを定義
let(:invocations) { [] }
# beforeではテストに必要なセットアップコードを記述
before(:each) do
subject.instance_eval do
define_hook :before_foo
define_hook :before_bar
end
end
describe '#clear_hooks' do
# letやbeforeのセットアップコードを使ってテストを実行する
it 'clears all hooks' do
subject.before_foo { invocations << :callback }
subject.clear_hooks
subject.invoke_hook(:before_foo, nil)
invocations.should be_empty
end
end
まとめ(と注意点)
さすがRSpecの開発者だけあって、let
の利点をうまくまとめてくれています。
特にtypoにすぐ気づけるというのはlet
の一番大きな利点だと思います。
ただし、気をつけるべき点もあります。
回答の中には書かれていませんが、let
は遅延初期化されるという特性があり、この動きを理解していないと「パスするはずのテストがパスしない」という問題を引き起こす場合があります。
# なぜかパスしないRailsのテストの例
let(:blog) { Blog.create(title: 'RSpec必勝法', content: 'あとで書く') }
it 'ブログの取得ができること' do
expect(Blog.first).to eq blog
end
上のようなテストはlet
の代わりにlet!
を使うとパスするようになります。
詳しくは以前書いたこちらのQiita記事を参考にしてみてください。