RSpecのletとlet!:遅延評価と即時評価の違いと使い分け

スポンサーリンク

RSpecのletとlet!:遅延評価と即時評価の違いと使い分け

はじめに

RSpecで頻繁に使う letlet! は、どちらもテストデータを定義するためのヘルパーです。しかし評価のタイミングが異なり、使い方を間違えるとテストが意図通りに動かないことがあります。

この記事では let の遅延評価と let! の即時評価の違いを具体例を使って解説します。


let — 遅延評価

let で定義したブロックは、最初に呼ばれたときに初めて評価されます。テスト内で一度も使われなければ、ブロックは実行されません。

RSpec.describe User, type: :model do
  let(:user) { User.create(name: "田中太郎", email: "tanaka@example.com") }

  it "名前を持つ" do
    expect(user.name).to eq("田中太郎")   # ここで初めて user が評価・作成される
  end
end

let はメモ化(memoize)されるため、同じテスト内で複数回 user を呼んでも、ブロックは1回しか実行されません。

it "同じオブジェクトを返す" do
  expect(user.object_id).to eq(user.object_id)   # 同じインスタンス
end

let! — 即時評価

let! で定義したブロックは、各テストの it ブロックが実行される前に必ず評価されます。テスト内で使わなくても実行されます。

RSpec.describe Post, type: :model do
  let!(:post) { Post.create(title: "サンプル記事") }

  it "投稿が存在する" do
    expect(Post.count).to eq(1)   # let! によって既にレコードが作成されている
  end
end

before { post } と等価で、事前にDBにレコードを用意したい場合に使います。


違いを比較する

RSpec.describe Post, type: :model do
  # let は呼ばれるまで評価されない
  let(:lazy_post)  { Post.create(title: "遅延") }

  # let! は it の前に必ず評価される
  let!(:eager_post) { Post.create(title: "即時") }

  it "レコード数を確認する" do
    # この時点で eager_post は既に作成済み
    # lazy_post はまだ作成されていない
    expect(Post.count).to eq(1)

    lazy_post   # ここで初めて lazy_post が作成される
    expect(Post.count).to eq(2)
  end
end

let! が必要なケース

let! が必要なのは、テスト内でそのオブジェクトを直接参照しないが、DBにレコードが存在していることを前提とする場合です。

RSpec.describe User, type: :model do
  let!(:existing_user) { create(:user, email: "tanaka@example.com") }

  context "同じemailで登録しようとした場合" do
    let(:new_user) { build(:user, email: "tanaka@example.com") }

    it "無効である" do
      # existing_user を直接使わないが、DBに存在していないと重複チェックが機能しない
      expect(new_user).not_to be_valid
    end
  end
end

このケースで let を使うと existing_user は評価されず、重複バリデーションのテストが意図通りに動きません。


let を使うべきケース

テスト内で明示的に使うオブジェクトは let で十分です。使わない場合にDBアクセスが発生しないため、テストが速くなります。

RSpec.describe User, type: :model do
  let(:user) { build(:user, name: "田中太郎") }

  it "有効である" do
    expect(user).to be_valid
  end

  it "名前を持つ" do
    expect(user.name).to eq("田中太郎")
  end
end

使い分けのまとめ

let let!
評価タイミング 初めて呼ばれたとき 各テストの前
使わない場合 実行されない 実行される
DBアクセス 必要なときだけ 必ず発生
向いているケース テスト内で直接使うデータ DBにレコードが必要な前提条件

基本は let を使い、「DBにレコードがないと成立しないテスト」だけ let! を使うのがシンプルです。


subject との関係

subject も遅延評価です。describe にクラスを渡すと subject は自動で User.new になります。

RSpec.describe User, type: :model do
  subject { build(:user, name: "田中太郎") }

  it { is_expected.to be_valid }
  it { is_expected.to have_attributes(name: "田中太郎") }
end

まとめ

  • let遅延評価。初めて呼ばれたときに評価され、同一テスト内ではメモ化される
  • let!即時評価。各 it の前に必ず実行される
  • DBにレコードが前提として必要な場合は let!、テスト内で直接使う場合は let

RSpecの基本的な書き方は「RSpec入門:インストールからモデルスペックの書き方まで」を参照してください。

テストを高速化する let_it_be については「RSpecのlet_it_be:test-profで高速化するDB生成の使い方」を参照してください。

gemのバージョン管理は「Bundler入門:Gemfile・bundle install・bundle execの使い方まとめ」を参照してください。

コードスタイルの自動チェックは「RuboCop導入入門:インストールから設定・git commit時の自動実行まで」を参照してください。