RSpecのlet_it_be:test-profで高速化するDB生成の使い方

スポンサーリンク

RSpecのlet_it_be:test-profで高速化するDB生成の使い方

はじめに

RSpecのテストでDBへのレコード生成が多くなると、let! の繰り返し実行がボトルネックになります。let_it_betest-prof gemが提供するヘルパーで、同じexample group内でレコードを1回だけ生成してテストを高速化できます。


let! の何が遅いか

let! は各 it の前に毎回評価されます。

RSpec.describe Post, type: :model do
  let!(:user)     { create(:user) }     # 各itの前に実行
  let!(:category) { create(:category) } # 各itの前に実行
  let!(:post)     { create(:post, user: user, category: category) }

  it "タイトルを持つ"  do ... end  # ← user, category, post を生成
  it "公開できる"      do ... end  # ← user, category, post を生成(また)
  it "下書きに戻せる"  do ... end  # ← user, category, post を生成(また)
end

itが3つあれば3回、10個あれば10回DBへのINSERTが発生します。大きなテストスイートではこれが積み重なり、実行時間が長くなります。


let_it_be の仕組み

let_it_be はexample group(describecontext)単位で1回だけレコードを生成し、各 it 間はトランザクションでラップして副作用を隔離します。

RSpec.describe Post, type: :model do
  let_it_be(:user)     { create(:user) }     # グループ全体で1回だけ生成
  let_it_be(:category) { create(:category) }
  let_it_be(:post)     { create(:post, user: user, category: category) }

  it "タイトルを持つ"  do ... end  # ← 生成済みのレコードを使う
  it "公開できる"      do ... end  # ← 生成済みのレコードを使う
  it "下書きに戻せる"  do ... end  # ← 生成済みのレコードを使う
end

itが3つでも10個でも、INSERTは1回ずつです。


インストール

test-prof gemをGemfileに追加します。

# Gemfile
group :test do
  gem 'test-prof'
end
bundle install

spec/spec_helper.rb または spec/rails_helper.rb に設定を追加します。

# spec/rails_helper.rb
require 'test_prof/recipes/rspec/let_it_be'

基本的な使い方

let!let_it_be に置き換えるだけで使えます。

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

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

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

ネストした context でも使える

RSpec.describe Post, type: :model do
  let_it_be(:user) { create(:user) }

  context "公開状態の場合" do
    let_it_be(:post) { create(:post, user: user, published: true) }

    it "公開されている" do
      expect(post.published?).to be true
    end
  end

  context "下書き状態の場合" do
    let_it_be(:post) { create(:post, user: user, published: false) }

    it "非公開である" do
      expect(post.published?).to be false
    end
  end
end

注意点:デフォルトでimmutable

let_it_be で生成したオブジェクトはデフォルトで変更不可(frozen)扱いになります。テスト内でオブジェクトの属性を変更しても、他のテストには影響しません。

let_it_be(:user) { create(:user, name: "田中太郎") }

it "名前を変更しても他のテストに影響しない" do
  user.name = "鈴木花子"   # メモリ上の変更
  expect(user.name).to eq("鈎木花子")
end

it "元の名前のまま" do
  expect(user.name).to eq("田中太郎")   # 影響を受けない
end

DBを更新した場合は reload: true

テスト内でDBを直接更新し、次のテストでもDBの状態を反映させたい場合は reload: true を使います。

let_it_be(:user, reload: true) { create(:user, name: "田中太郎") }

it "DBを更新する" do
  user.update!(name: "鈴木花子")
  # 次のit開始前にuser.reloadが自動で呼ばれる
end

it "常にDBの最新状態を参照する" do
  expect(user.name).to eq("田中太郎")   # reloadされて元の値に戻っている
end

再取得が必要な場合は refind: true

オブジェクトをDBから再取得(新しいインスタンスとして)したい場合は refind: true を使います。

let_it_be(:post, refind: true) { create(:post) }

reload: true は同じオブジェクトをリロード、refind: true は新しいオブジェクトとして取得し直す違いがあります。


let・let!・let_it_be の比較

let let! let_it_be
評価タイミング 初めて呼ばれたとき 各itの前 グループで1回だけ
DB生成回数 呼ばれた回数 itの数 × 1回 1回
it間の副作用 なし トランザクションでロールバック トランザクションでロールバック
向いているケース 軽い計算・オブジェクト DBが必要な前提条件 変更しない共通レコード

どこに使うか

let_it_be はすべてのケースで使えるわけではありません。

向いているケース: - テスト内でレコードを更新・削除しない共通の前提データ - 親レコード(User・Categoryなど)の生成

向いていないケース: - テスト内でレコードを更新・削除する場合(let!reload: true を使う) - テストごとに異なる状態のレコードが必要な場合

基本は let を使い、DBレコードが必要で変更しない共通データは let_it_be、テストごとに状態が変わるものは let! という使い分けが実践的です。


まとめ

  • let_it_be はexample group単位でDBレコードを1回だけ生成する
  • let! の代替として使うとテストの実行速度が大幅に改善できる
  • デフォルトでimmutableなため他のテストへの副作用がない
  • DBを更新する場合は reload: true、再取得は refind: true を使う

letlet! の基本的な違いは「RSpecのletとlet!:遅延評価と即時評価の違いと使い分け」を参照してください。

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