🤖

FactoryBotアンチパターン8選

2024/11/20に公開

これはなに

社内で共有した資料をほぼそのまま公開します。
僕が数年間(8年くらい? 共有してから月日がたち11年くらいになった?)FactoryBot(旧:FactoryGirl)を使ってきてアンチパターンだと思っているものを紹介します。
そもそもFactoryBotを使うのが……という話をしたい方は別のところでお願いします。
そのうちZennとかにまとめたい気持ちです、と言ってから3年弱くらい経ちましたが供養のために公開しておきます。

アンチパターン

パターン1. デフォルト値が固定値のfactory

FactoryBot.define do
  factory :user do
    name { "yuji_developer" }
  end
end

問題点

Factoryのデフォルト値に依存したテストが書かれがちになります。
またテストに直接関係ない属性を書き換えがちになります。

解決策

Fakerを使うなり Time.current などの動的な値が返るものを利用するなりして出来るだけランダム値を設定しましょう。

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
  end
end

パターン2. 複数回利用できないfactory

class User
  validates :email, uniqueness: true
end

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { "foo@example.com" }
  end
end

問題点

複数回利用できない時点で問題ですが、こういうものがあるとテストに関係ない場合でもその属性を指定してテストが書かれたりします。

解決策

Fakerを使うなり sequence を使うなりしましょう。
個人的には sequence はRails consoleで開発用のデータを作るときに相性が悪いので避けています。
なお、Fakerを使う場合は unique を使うと重複が発生しにくくなります。

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.unique.email }
  end
end

パターン3. それ自身で完結していないfactory

他の値を指定しないと使えないパターンです。

class User
  validates :name, presence: true
  validates :email, presence: true
end

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
  end
end

問題点

例では毎回 email を指定して create(:user, email: "foo@example.com") のようにする必要があります。
テストに関係ない属性でも毎回指定する必要があり使いにくいですしテストが読みにくくなります。

解決策

何も指定せずに create(:user) などとして使えるようにしましょう。

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.unique.email }
  end
end

パターン4. 矛盾したfactory

あり得ない値の組み合わせになってしまうパターンです。

難しい……。

問題点

使いにくいですしテストが通らなくなったりします。

解決策

あり得ない組み合わせにならないようにしましょう。

パターン5. buildbuild_stubbed でDB書き込みしているfactory

build_stubbed したのに関連が書き込まれてしまうパターン

class Post < ApplicationRecord
  belongs_to :author, class_name: "User"
end

FactoryBot.define do
  factory :post do
    author { create(:user) }
  end
end

問題点

build_stubbed はDB書き込みをしないことでテスト実行速度を向上するために使われますがこの例のようなfactoryになっているとDB書き込みが行われてしまうためメリットを享受できません。

解決策

association を使いましょう。
これによって build_stubbed(:post) した場合は authorbuild_stubbed されますし、 create(:post) した場合は authorcreate されます。

FactoryBot.define do
  factory :post do
    association :author, factory: :user
    # または
    # author factory: :user
  end
end

パターン6. デフォルトで関連作りすぎのfactory

has_manyhas_one 関連のレコードがデフォルトで作られるパターンです。

class User < ApplicationRecord
  has_many :posts, inverse_of: :author
end

class Post < ApplicationRecord
  belongs_to :author, class_name: "User"
end

FactoryBot.define do
  factory :user do
    after(:create) do |user, _evaluator|
      create_list(:posts, 10, author: user)
    end
  end
end

FactoryBot.define do
  factory :post do
    association :user, factory: :user
  end
end

問題点

テストに不要なことが多いレコードが毎回作られてしまい速度劣化の元になります。
また has_manyhas_one で作られる側のfactoryを使う際に意図しないレコードが作られるようになりがちです。例だと create(:post) すると post が11レコード出来てしまう)

解決策

必須の belongs_to 関連以外はデフォルトで作成されないようにします。
注意したいのは特定の状態の時にだけ作られる関連があったときにtraitで他の属性と同時に作ってしまいがちなことです。これも複数のtraitに分けると良いでしょう。(例: xxxxxx_with_yyy など)
また、複数の関連を作る場合は数を指定可能にしておくと良いでしょう。

FactoryBot.define do
  factory :user do
    trait :with_posts do
      transient do
        post_count { 1 } # レコード数に意味があるような場合はランダム値にすることもあります
      end

      after(:create) do |user, _evaluator|
        create_list(:posts, evaluator.post_count, author: user)
      end
    end
  end
end

パターン7. レコードの状態を表すtraitや子のfactoryを提供していないfactory

難しい……。

問題点

特定のレコードの状態を作る場合に毎回値セットの組み合わせを指定するのは大変ですしfactoryを使うのが難しくなります。

解決策

traitやそれを組み合わせた子factoryを作りましょう。

パターン8. アプリケーションロジックに依存しているfactory

難しい……。

問題点

アプリケーションロジックの変更でfactoryが意図しない挙動をするようになったりします。

解決策

factoryのことはfactory内で完結するようにします。
factoryに限らずテストデータの準備にアプリケーションロジックを使うのは避けましょう。

僕が考えた最強のfactory作成方針

いまは違う気持ちのものもあるかもしれないけど書いた当時の内容そのまま出しておきます。

factory作成方針

  • factoryはそれ自身で完結し何も考えずにcreateしてレコードを作成できる
  • 各カラムのデフォルト値は出来るだけランダム値とする
    • nullableなカラムの場合はランダム値の候補にnilを含める
    • 特定の値を設定したい場合はtraitを利用する
    • 原則としてデフォルトで active { true } のようなことをしない
    • ただし削除フラグなど通常は切り替わらないようなものはデフォルトで固定値を設定するのを容認する
  • DBのカラムのデフォルト値にできるだけ頼らない
  • traitはシンプルな状態を表す
  • テストに関係ないカラムを個別指定しなくても使えるfactoryにする
  • ルートになるfactoryはそのレコードが作成された初期状態に近いものにする
    • ネストしていないかつparentを指定していないfactory
    • 通常はモデル名と同じ名前
  • traitを組み合わせて特定の状態を表す子factoryを用意する
    • それが必要ない程度にシンプルなモデルの場合は作成しなくても良い
  • ルートfactoryhas_one/has_many関連など必須のbelongs_to関連以外をデフォルトで持たない
    • 関連を作る場合はtraitを指定する
  • 作成される関連がデフォルトの状態で矛盾しないようにする
    • A belongs to B, A belongs to C, B belongs to Cと関連がある場合にAからCとBからCの関連が矛盾しないようにする
  • buildしただけで関連がcreateされないようにする
    • 関連作成時にstrategycreateを指定しない
    • テスト実行速度のためにbuild/build_stubbedを使いやすくする
  • factory内で出来るだけアプリケーションのロジックを呼び出さない
  • データの状態にパターンがある場合はルートのfactoryをそのまま使うのを避ける
    • データの状態に名前をつけた子のfactoryを作る
      • e.x. メンバー、管理者ユーザー、etc...
    • 名前を付けることで指定するパラメーター数が減らなくても指定することでデータの状態が表せるという価値がある

factoryとtraitの使い分け

factoryを使うと良いもの

  • 性質が違うものを使い分ける場合
    • メンバー、管理者ユーザーを使い分けるようなシーン
  • 複数のtraitを組み合わせて特定の状態を作る場合
    • traitの組み合わせのショートカット的な位置づけ

traitを使うと良いもの

  • シンプルな状態を表す場合
    • 決済成功した注文、決済失敗した注文
  • 特定のカラムの値を固定値にする場合
    • メルマガ受信設定オン、メルマガ受信設定オフ

命名規則

factory

  • ルートfactoryはモデル名と合わせる
  • factoryにはモデル名を含めると良い
# ルートfactory
factory :user do
  # 子factory
  factory :user_xxx, traits: %i[with_credit_card with_address]
end

trait

  • with_xxx は別レコードを作る場合など自身のレコード以外を作成する場合に使う
    • 例: :with_credit_card
    • 特定のカラムに値を設定するだけの場合には使わない
      • 例: ステータス値を設定するなど
  • :<状態名>
    • 状態を表すtraitに使う
      • 例: :active
    • 設定されるのは1カラムとは限らない
  • :<カラム名>_<設定値>
    • 特にenum値を設定するtraitに使う
      • 例: :status_active
# 別レコードを作るtrait
trait :with_credit_card do
  credit_card do
    association(:credit_card, user: instance)
  end
end

# 特定のカラムに値を設定するためのtrait
trait :status_active do
  active { true }
end

# 状態を表すtrait
trait :active do
  active { true }
  inactive_at { nil }
end

参考資料

まとめ

がんばれ!

Discussion