📚

【フロントエンドが初めて触る Rails 】Rspecでのletを使った遅延評価とは

2024/10/13に公開

概要

業務でRSpecを使ってテストを実装している際に、let!(:hoge)でモデルのインスタンス生成処理を書けばいいんだな、と思いながら実装していました。すると、先輩エンジニアから「letとlet!には遅延評価の違いがある」との助言をもらいました。

その時は、「どんな場合に使い分ければいいんだ?」とすぐにはピンとこなかったので、今回しっかり調べて知識として整理してみようと思います。

letとlet!

  • let:実際に参照される時に評価されインスタンスが生成される。
    • 遅延評価
  • let!:テストが実行される前にすぐに評価されインスタンス生成される。
    • 即時評価

let

letで書いておくと必要な時にのみ評価されるため、不要な計算が発生しなかったり、同じテスト内ではキャッシュされた値が使われことでパフォーマンス的に良いとのこと。

RSpec.describe 'letの遅延評価' do
  let(:value) { puts "値を計算中"; 42 }

  it 'test1' do
    # 参照されるタイミングで初めて評価され生成される
    expect(value).to eq(42)
    # 同じテスト内では、ブロックは再実行されずキャッシュされた値が使われる
    expect(value).to eq(42)
  end

  it 'test2' do
     # このタイミングでもう一度評価され生成される 
    expect(value).to eq(42)
  end
end

let!

逆にlet!ではテストが実行する前にインスタンスが生成されるため、そのテストブロックが終わるまで保持し続けるため、パフォーマン的には遅延評価よりは下がるとのこと。

ただ、調べていくと、let!じゃないと意図したテストにならない場合があるということがわかった。

let!を使うべき場面

下記のテストでは、letで定義したbookが評価されるタイミングではまだ生成されていないためエラーになる。
そのため、パフォーマンスだけを考え、letを使うとテストでつまづくことも多くありそう。

# models/author.rb
class Author < ApplicationRecord
  has_many :books
end

# models/book.rb
class Book < ApplicationRecord
  belongs_to :author
end
let(:author) { FactoryBot.create(:author) }
let(:book) { FactoryBot.create(:book, author: author, title: 'hoge') }

it 'test3' do
  expect(author.books.first.title).to eq 'hoge'
end

まとめ

今回はletlet!の違いをまとめてみました。
安定してテストを実行できることを重要視すると、パフォーマンスにシビアにならない限りは基本的にはlet!が推奨かもしれません。
遅延評価はパフォーマンス的には良いかもしれませんが、使い所に少し気をつけるべきだなと感じました。

参考

https://qiita.com/kekke-n/items/2f5618753efba7799a55
https://github.com/willnet/rspec-style-guide?tab=readme-ov-file#letとletの使い分け

Discussion