letとlet!の呼び出し方の違い

let と let!

Use let to define a memolized helper method. The value will be cached across multiple calls in the same example but not across examples.

letで定義したメソッドは、同じdescribeの中であれば何度でも呼び出すことが出きる。

Note that let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked.

letは遅延評価を採用している。なので、letで定義したメソッドは、そのメソッドが最初に呼び出されるまでは評価されることがない。

describe 'let' do
  let(:user) { create(:user) }
  let(:user_article) { create(:article, user_id: user.id) }

  specify 'User が Article を持っていること' do
    expect(user.articles.first).to eq user_article
  end
end

上記のような場合、user.articles.firstを呼び出した時点では、create(:user) は実行されていない為、下記のようなFailure/Errorを吐き出す。

Failure/Error: expect(user.articles.first).to eq user_article
expect[].to eq user_article
//expectの中身がありませんというエラーが出てしまう。

全てのメソッドの前で実行させるためには、感嘆詞をつけてlet!にしてあげる。

describe 'let' do
  let!(:user) { create(:user) }
  let!(:user_article) { create(:article, user_id: user.id) }

  specify 'User が Article を持っていること' do
    expect(user.articles.first).to eq user_article
  end
end

こうすることで、userインスタンスとarticleインスタンスが expect(user.articles.first)とuser_articleの前で呼び出されるので、テストをパスさせることが出来る。

 create(:user) 
 create(:article, user_id: user.id)

引数を複数使うこともできる

letの中身は、引数を複数設定することができる。

例えば以下の場合、spec/factories/article.rbにおいてtraitを用いて定義した、pastとwith_sentenceをpast_article_with_sentenceという別名をつけて定義している。

spec/factories/article.rb

FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "title-#{n}" }
    sequence(:slug) { |n| "slug-#{n}" }
    category
  end

trait :past do
    published_at { DateTime.now.ago(1.hours) }
    state { :published }
  end

 trait :with_sentence do
    transient do
      sequence(:sentence_body) { |n| "test_body_#{n}" }
    end
    after(:build) do |article, evaluator|
      article.sentences << create(:sentence, body: evaluator.sentence_body)
    end
  end
end
aritcle.rb

describe "検索機能" do
  let(:past_article_with_sentence) { create(:article, :past, :with_sentence, sentence_body: 'こんにちは') }

it do
  visit edit_admin_article_path(past_article_with_another_sentence.uuid)
end

let and let!

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

rspecのbefore(:all)の注意点 - Qiita