RSpecのlet_it_be:test-profで高速化するDB生成の使い方
はじめに
RSpecのテストでDBへのレコード生成が多くなると、let! の繰り返し実行がボトルネックになります。let_it_be は test-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(describe・context)単位で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を使う
let と let! の基本的な違いは「RSpecのletとlet!:遅延評価と即時評価の違いと使い分け」を参照してください。
RSpecの基本的な書き方は「RSpec入門:インストールからモデルスペックの書き方まで」を参照してください。