test-profを試していたら、let!の挙動を勘違いしていることに気がついた

これは何

test-profというGemを試してみた。let!の挙動を勘違いしていることに気がついた。

test-profとは

テストのパフォーマンスを計測できるGem。

テストからのフィードバックの速さは開発者の生産性に影響するので、パフォーマンス改善は重要。DiscourseやDev.toなどのOSSで活用されている。

Install

group :test do
  gem "test-prof", "~> 1.0"
end

テストパフォーマンスを計測

EVENT_PROF, FPROF 変数を渡すと、factory-botでどれくらいデータを生成しているかを計測し、結果を表示してくれる。

require 'rails_helper'

RSpec.describe Article, type: :model do
  let!(:articles) { create_list(:article, 10) }

  it { expect(true).to eq true }
  it { expect(true).to eq true }
  it { expect(true).to eq true }
  it { expect(true).to eq true }
end
EVENT_PROF=factory.create FPROF=1 bundle exec rspec /spec/models/article_spec.rb
[TEST PROF INFO] Factories usage

 Total: 40
 Total top-level: 40
 Total time: 00:00.137 (out of 00:00.552)
 Total uniq factories: 1

   total   top-level     total time      time per call      top-level time               name

      40          40        0.1377s            0.0034s             0.1377s            article

let!の挙動を勘違いしていた

上記を走らせたときarticlesは最初だけ評価されて、Factoryの生成は10では?と最初思っていた。 idとcountを出してみる。

it do
  expect(true).to eq true
  puts "count: #{Article.count}"
  puts Article.pluck(:id)
end

it do
  expect(true).to eq true
  puts "count: #{Article.count}"
  puts Article.pluck(:id)
end
bundle exec rspec /spec/models/article_spec.rb
count: 10
261
262
...
270
  is expected to eq true
count: 10
271
272
...
280

自分がlet!の挙動を勘違いしていたことに気づいた。 let!はすべてのexampleの前で呼び出される。RubyMineを使っていれば、RSpec logからも挙動を確認できる。

なので、共通で使うデータをlet!で定義して、その下で context や it を書きまくるとINSERTが何度も走るので、テストのパフォーマンスが落ちる。

パフォーマンスを改善するには

共通で使うデータを一度だけINSERTされるようにするには、どうすれば良いか。
結論、test-profが提供しているbefore_allが良さそうな気がしている。

rails_helper.rb
require "test_prof/recipes/rspec/before_all"
before_all do
  create_list(:article, 10)
end

ちなみに、rspecがbefore(:all)というのを提供しているが、これはDBにデータが残り続けるので厄介そうなので使いたくない。

Source code