はじめに
こんにちは、プレックスの種井です。
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を利用する
- 何でもcreate(…)にしない
設定
クラス名を省略して、メソッドの呼び出しをする
公式のセットアップガイドにもありますが、rails_helper.rb
にFactoryBot::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
終わりに
公式ドキュメントや先人達がベストプラクティスとして公開してくださっている資料を参考に、まず弊社チームでも取り組んでみるとよさそうなものをピックアップし、ガイドの作成を行いました。
今回作成したものをベースに運用しつつ、その中でさらに取り入れたものや、工夫したものは今後もブログなどで紹介したいと思います。
最後になりますが、プレックスではソフトウェアエンジニア、フロントエンドエンジニアを募集しています。
上述のような、開発上の取り組みや課題に対してご一緒していただける方、少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。