「let・let!・before」の違いと実行順序(RSpec)

【結論】

let/let!インスタンス変数を定義する際、beforeより分かりやすく記述できる

beforeインスタンス変数の定義以外(メソッドの実行など)でも利用することができる

letは変数が初めて使用された時に評価(遅延評価)され、let!beforeはブロック宣言時に評価される。

【目次】

【本題】

let・let!・beforeの役割について

let・let!・beforeは、いずれもRSpecにおいて、各exampleで共通の処理を定義することができます。

これによりそれぞれのテストに同じ記述をする手間が省け、DRYな状態をキープできます。

なお、これらは意識していないと、どれも同じ動きをしている様に見えますが、実際には挙動や使用する場面が異なります。

今回はそれをまとめます。

「let・let!」はインスタンス変数の定義に用いる

各exampleで共通利用するインスタンス変数を定義する際、beforeを利用すると、以下の様なコードになります。

  before do
    @user = create(:user, name: 'alice')
    @post = create(:post, title: 'test')
  end

これをletで書き換えると以下の様になります。

let(:user)   { create(:user, name: 'alice') }
let(:post)   { create(:post, title: 'test') }

letの方がやや見通しが良くなったと思います。

この事からインスタンス変数の定義を共通化する際には、let・let!を使用するのが一般的の様です。

「before」はメソッドを実行する際に用いる

ではbeforeはどの様な場面で用いるかというと、インスタンス変数の定義以外で利用します。

例えば、以下の様にサインインの処理を記述すれば、各exampleが実行される前に共通して処理が走ります。

before { sign_in(current_user) }

let・let!では、この様な使い方は出来ないので、beforeが活躍します。

「let」と「let!」の実行順の違い

なおletlet!については、それぞれの実行順に注意する必要があります。

letは変数が初めて使用された時に評価(遅延評価)されます。

let!はブロック宣言時に評価される。

例えば、以下のコードは実行順の関係で、正常に完了しません。

let(:post)   { create(:post, title: 'test') }
it 'ポストが取得できること' do
  expect(Post.first).to eq post
end

Post.firstが呼び出された時点では、postが生成されていない(呼び出されていない為)ことが原因です。

これを回避するにはlet!を用います。

let!(:post)   { create(:post, title: 'test') }
it 'ポストが取得できること' do
  expect(Post.first).to eq post
end

これにより、Post.firstが呼び出された時点では、postが生成されるので、正常にテストが通ります。

参考情報

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

RSpec の letとlet!とbeforeの挙動と実行される順番 - Qiita