FactoryBot運用ガイドを作りました

はじめに

こんにちは、プレックスの種井です。

PlexJob開発チームではRSpecによるテストに使用するfixtureの作成に、FactoryBotを使用しています。 テストコードに対してはrubocop-rspecにより一定のルールに則ったコードが作成されていますが、Factoryの定義やオブジェクトの生成方法などは個々のメンバーに委ねられており、オンボーディングやコードレビューの際に方針に対して方針に対して疑問が上がる箇所になっていました。 今回、所属するチーム向けに運用やコーディングのルールを作成したので、この場を借りて紹介したいと思います。

目次

  • はじめに
  • 目次
  • 運用ガイド
    • 運用の観点
    • 運用時のルール
  • 終わりに
  • 参考資料

運用ガイド

前提と運用の観点

運用するにあたっての観点として「可読性」や「再利用性」はもちろんですが、DBへのアクセスを伴うこともあるため「パフォーマンス」にも配慮する必要があります。

また、オペレーションやコーディングの作業上のどの部分で考慮すべきかがわかりやすいように、各方針やルールを「設定」、「定義」、「作成」に分類しました。

運用時のルール

  • 設定
    • FactoryBot::Syntax::Methodsを設定、クラス名を省略して、メソッドの呼び出しをする
    • FactoryBot公式のlinterを使う
  • 定義
    • ファイル名は複数形にする
    • 例: Userを定義する場合はusers.rbにする
    • 1ファイル1定義にする
    • デフォルトデータはなるべく最小限かつ簡潔にする
      • テストに必要なattributeのみを定義する
      • traitでhook(after(…))を使用して、関連データ(has_manyな)の作成はオプションにする
      • 特殊な状態の最小単位をtraitで定義する
        • ただし、なんでもtraitにせず汎用的なデータ状態のみに限る
    • 固定値ではなく本物に近いデータを定義する
      • sequenceやFakerの活用を検討する
  • 作成
    • 何でもcreate(…)にしない
      • association先はassociationで定義する
    • 1度に複数のオブジェクトが必要になる場合はbuild_list,create_listを利用する

設定

クラス名を省略して、メソッドの呼び出しをする

公式のセットアップガイドにもありますが、rails_helper.rbFactoryBot::Syntax::Methodsを定義することで、クラス名を省略してメソッドを呼び出すことができます。

# rails_helper.rb

RSpec.configure do |config| 
  config.include FactoryBot::Syntax::Methods 
end
# クラス名(FactoryBot)を省略してメソッドを呼び出します。

# build 
user = build(:user) 

# create 
user = create(:user)

FactoryBot公式のlinterを使う

FactoryBot.lintを実行することで、定義された全Factoryに対してcreateを実行し、データの作成時に例外が発生する場合は、該当するFactoryの一覧とともにFactoryBot::InvalidFactoryErrorを例外として投げてくれます。

未定義の必須フィールドのような、Factoryのデータ定義の不備を事前に検知することができます。

公式にあるようにrakeタスクを作成して、GitHub Actionsなどから呼び出すことが推奨されています。

# lib/tasks/factory_bot.rake

namespace :factory_bot do
  desc "Verify that all FactoryBot factories are valid"
  task lint: :environment do
    if Rails.env.test?
      conn = ActiveRecord::Base.connection
      conn.transaction do
        FactoryBot.lint
        raise ActiveRecord::Rollback    
      end
    else
      system("bundle exec rake factory_bot:lint RAILS_ENV='test'")
      fail if $?.exitstatus.nonzero?
    end
  end
end
$ bundle exec rake factory_bot:lint RAILS_ENV='test'

定義

ファイル名は複数形にする

例として、Userモデルが定義されている場合に対応するFactoryのファイル名はusers.rbにします。

1ファイル1定義にする

例として、users.rbを作成する場合

FactoryBot.define do 
  factory :user do # 1ファイルに対して1定義にする
    ... 
  end
end

デフォルトのFactory定義はなるべく最小かつ簡潔にする

テストに必要なattributeのみを定義し、使用しないattributeまでデフォルトのデータを定義しないようにします。

traitでhook(after(...))を使用し、has_manyな関連データの作成はオプションとして呼び出し時に選択できるようにします。

FactoryBot.define do
  factory :user do
    trait(:with_posts) do # 最小単位にする&オプションとして、使用時に選択できるようにする
      transient do
        posts_count { 5 }
      end

      # has_manyな関連データの作成
      after(:create) do |user, evaluator|
        create_list(:post, evaluator.posts_count, user: user)
        user.reload
      end
    end
  end
end

使用頻度が高い、特定の状態はtraitで定義しておきます。

ただし、なんでもtraitにするのではなく、汎用的なデータ状態のみに限ります。

FactoryBot.define do
  factory(:post) do
    trait :published do
      published { true }
    end  

    trait :unpublished do
      published { false }
    end
    
    trait :week_long_publishing do
      start_at { 1.week.ago }
      end_at { Time.now }
    end
    
    trait :month_long_publishing do
      start_at { 1.month.ago }
      end_at { Time.now }
    end

    factory :week_long_published_post, traits: [:published, :week_long_publishing]
    factory :month_long_published_post, traits: [:published, :month_long_publishing]
    factory :week_long_unpublished_post, traits: [:unpublished, :week_long_publishing]
    factory :month_long_unpublished_post, traits: [:unpublished, :month_long_publishing]
  end
end

固定値ではなく本物に近いデータを定義する

一意性の必要な値にはsequenceの利用を検討します。

また、本物に近い適当な値を設定したい場合はFakerの利用を検討します。

FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "person#{n}@example.com" } # sequence
    
    name { Faker::Name.name } # Faker
  end
end

作成

何でもcreate(...)にしない

createメソッドは毎回SQLが実行されるため、テスト実行のパフォーマンスに影響を与えます。

モデルのバリデーションなど、DBに値が生成されている必要がないテスト対象の場合はbuildメソッドをはじめとした、メモリ上にオブジェクトを作成する機能を使用します。

build(:user)
build_stubbed(:user) # buildで済ましたいが、idやtimestampが必要な場合はbuild_stubbedを使用する

association先を事前に作成する場合は、associationで定義しておくことが推奨されます。

createメソッドを使用してassociation先を作成すると、buildした際に常にcreateが伴ってしまうためです。

FactoryBot.define do 
  factory :post do
    user # belongs_toの関連(associationの省略記法) 
    user { create(:user) } # NG postをbuildした場合でもuserがcreateされてしまう
  end
end

また、作成時にかかる処理はinstance_double,spy(RSpecの機能) > build(…) > create(…) の順で速いです。

1度に複数のオブジェクトが必要になる場合はbuild_list,create_listを使用する

必要以上にオブジェクトを作成しないようにします。

また、transientを使用して、作成数に対してラベル付けをしておくようにします。

FactoryBot.define do 
  factory :user do
    trait(:with_posts) do
      transient do
        posts_count { 5 } 
      end

      # 必要な分だけ作成する 
      after(:create) do |user, evaluator| 
        create_list(:post, evaluator.posts_count, user: user) 
        user.reload 
      end 
    end 
  end 
end

終わりに

公式ドキュメントや先人達がベストプラクティスとして公開してくださっている資料を参考に、まず弊社チームでも取り組んでみるとよさそうなものをピックアップし、ガイドの作成を行いました。

今回作成したものをベースに運用しつつ、その中でさらに取り入れたものや、工夫したものは今後もブログなどで紹介したいと思います。

最後になりますが、プレックスではソフトウェアエンジニアフロントエンドエンジニアを募集しています。

上述のような、開発上の取り組みや課題に対してご一緒していただける方、少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。

参考資料