RSpecのletとlet!:遅延評価と即時評価の違いと使い分け
はじめに
RSpecで頻繁に使う let と let! は、どちらもテストデータを定義するためのヘルパーです。しかし評価のタイミングが異なり、使い方を間違えるとテストが意図通りに動かないことがあります。
この記事では 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時の自動実行まで」を参照してください。