こざテク

機械系エンジニア→Web系エンジニアを目指して勉強中

FactoryBotのbuildとcreate

まいど、りゅういちです。
今日は勉強しているRSpecで使うFactoryBotの復習です。

buildメソッドとcreateメソッド

Everyday Rails - RSpecによるRailsテスト入門には以下のような説明がありました。

次のことを覚えてください。
FactoryBot.build を使うと新しいテストオブジェクトをメモリ内に 保存します。FactoryBot.create を使うとアプリケーションのテスト用データベースにオブジェ クトを永続化します。(P.55)

他にも。

可能な限り FactoryBot.create よりも FactoryBot.build を使ってください。こうすればテストデータベ ースにデータを追加する回数が減るので、パフォーマンス面のコストを削減できます。(P.68)

buildとcreatetの違いや使い分けは以下のようになります。

build

メモリ上にインスタンスが作られます。 'build'メソッドで呼び出されたときにインスタンスが生成され、テストを抜けるとインスタンスは消滅します(下図がわかりやすい)

f:id:Zakku44:20201022013709p:plain
buildでメモリ上にインスタンスが作られるとは

コード例はこんな感じ。
spec/factories/tasks.rbはこれ。

FactoryBot.define do
  factory :task do
    title { 'Task' }
    content { "content" }
    deadline { Random.rand(from..to) }
  end
end

っで、spec/system/task_spec.rbはこれ。

require 'rails_helper'

RSpec.describe Task, type: :system do
  describe 'Task一覧' do
    context '正常系' do 
      it "一覧ページでTaskが表示されること" do
        task = build(:task)  #---①
        visit tasks_path
        expect(current_path).to tasks_path
        expect(page).to have_content task.title
        expect(page).to have_content task.content
      end
    end
  # 以下にテストコードが続く

①のtask = build(:task)it "is valid with all attributes" do〜〜end内で定義されています。buildではDBに保存されず、テストを抜けるとメモリ上からインスタンスが消滅することになるので、別のテスト(it "hgehoge"~)ではtaskの読み込みはできません。
DBに格納されていないことは、コンソール上でcreated_atupdated_atnilになっていることから確認できます。

[1] pry(#<RSpec::ExampleGroups::Task::Task::Nested>)> task = build(:task)
=> #<Task:0x00007f9aaf093318
 id: nil,
 title: "Task",
 status: "todo",
 deadline: Thu, 26 Sep 2019 00:00:00 JST +09:00,
 created_at: nil,
 updated_at: nil>

したがって、上記で挙げたようなDBアクセスが必要ないテストでは`create'ではなく'build'メソッドを使います。
というのも、課題でやるぐらいのテスト量では問題にならないかもしれませんが、実務ではテスト処理のパフォーマンスも求められるようなので、負荷を軽くする意識が必要みたいです。

create

DB上にインスタンスを作成します(インスタンスの永続化ってやつ)
したがって、DBにアクセスしてデータを取ってくる必要がある場合(データの個数、削除、カラムのユニークに対するテストなど)はcreateを使います。
コード例はこんな感じ。

describe 'Task削除' do
  context '正常系' do
    it 'Taskが削除されること' do
      task = create(:task)  #---②
      visit tasks_path
      click_link 'Destroy'
      page.driver.browser.switch_to.alert.accept
      expect(current_path).to eq tasks_path
      expect(current_path).to have_content('タスクが削除されました')
      expect(current_path).not_to have_content task.title
    end
  end
end

②でtask = create(:task)でDBにtaskが格納されています。
そのためtasks_pathにアクセスすると、taskに対応したclick_link 'Destroy'が出現します。
createではなくbuildtaskを定義した場合には、click_link 'Destroy'が出現しないため、ここでエラーが起こります。
コンソールで確認すると、DBに格納されていることが分かります。

[2]pry(#<RSpec::ExampleGroups::Task::Task::Nested>)> task = create(:task)
=> #<Task:0x00007f9ab6275620
 id: 1,
 title: "Task",
 status: "todo",
 deadline: Tue, 27 Aug 2019 00:00:00 JST +09:00,
 created_at: Tue, 27 Oct 2020 08:14:50 JST +09:00,
 updated_at: Tue, 27 Oct 2020 08:14:50 JST +09:00>

参考文献

FactoryBotにおけるcreateとbuildの違い
Relish

まとめ

RSpecでもコンソールが使って都度確認すると理解度がめっちゃアップしました
letlet!beforeの挙動の違い、「遅延評価とは?」もコンソールを使って確認してる感じで書かこうと思います。