FactoryBotテクニック

FactoryBot.define do
  factory :model_name do
    field_name { 'value' }

    factory :factory_name, class: ModelName do
      field_name { 'value' }
    end

    # has_many関連
    factory :factory_name2, class: ModelName do
      transient do
        count { 5 }
      end
      after(:build) do |model, evaluator|
        model.has_many_models << build_list(:has_many_model, evaluator.count)
      end
    end
  end
end

ケースごとにfactoryを分ける

以下のような1つのfactoryを色々なところで使おうとすると、ニーズが衝突して手に負えない状況に陥りやすい。

FactoryBot.define do
  factory :user do
    login_name { 'account' }
    premium { true } # 有料会員かどうか
    # ...
    transient do
      blogs_count { 5 }
    end
    after(:build) do |user, evaluator|
      user.blogs << build_list(:blog, evaluator.blogs_count)
    end
  end
end

有料会員と無料会員を振り分けたい場合、テスト側でケアしなければならない。他のカラムやアソシエーションが影響を受けるなら、それらもテスト側で処理しなければならなくなる。

また、常にアソシエーション(上記の例ならuser.blogs)も生成されることになるため、パフォーマンスが低下する。

そのため、以下のようにfactoryを分けた方が良い。

FactoryBot.define do
  factory :user do
    login_name { 'account' }
    premium { false }
    factory :normal_user, class: User do
      factory :normal_user_has_blogs, class: User do
        user_has_blogs
      end
    end
    factory :premium_user, class: User do
      premium { true }
      factory :premium_user_has_blogs, class: User do
        user_has_blogs
      end
    end
    trait :user_has_blogs do
      transient do
        blogs_count { 5 }
      end
      after(:build) do |user, evaluator|
        user.blogs << build_list(:blog, evaluator.blogs_count)
      end
    end
  end
end

traitはテスト側から参照しない

テスト側からtraitを参照しない方が良い。参照したとしても、レゴブロックのようにtraitを組み合わせるのではなく、すでにあるfactoryをチューニングするスイッチとして使った方が良い。

例えば、以下のようなfactoryを考える。

FactoryBot.define do
  factory :user do
    login_name { 'account' }
    premium { false }
    trait :normal_user
    trait :premium_user do
      premium { true }
    end

    trait :user_has_blogs do
      transient do
        blogs_count { 5 }
      end
      after(:build) do |user, evaluator|
        user.blogs << build_list(:blog, evaluator.blogs_count)
      end
    end
  end
end

このfactoryを利用するなら、create(:user, :premium_user, :user_has_blogs, login_name: 'suzuki')のようになる。もし、user_has_blogsnormal_user用のtraitでpremium_useruser_has_blogsの組み合わせがデータ不整合を起こしていたとしても、テストが失敗しないかぎりは間違いに気づくことは難しいだろう。

このように、traitをレゴブロックのように組み合わせる設計をしてしまうと、factoryを利用する側に組み合わせの整合性の責任を負わせることになる。そして、factoryはテストのあらゆる場所から利用されることを考えれば、まずい設計であることは明らかである。

したがって、整合性の責任はfactoryの定義内で負うべきである。factory :normal_userfactory :normal_user_has_blogsのように整合性を持つ単位でfactoryを用意した方が良い。

traitを定義するとしても、factoryから参照するだけでテスト側から参照しないようにするか、整合性に影響しないちょっとした設定を変更するスイッチとして利用した方が良い。

has_many関連の定義

has_many関連の定義は以下のようにする(traitにするかは別として)。

FactoryBot.define do
  factory :user do
    trait :user_has_blogs do
      transient do
        blogs_count { 5 }
      end
      after(:build) do |user, evaluator|
        user.blogs << build_list(:blog, evaluator.blogs_count)
      end
    end
  end
end

user.blogs << build_list(:blog, evaluator.blogs_count)ではなくcreate_list(:blog, evaluator.blogs_count, user: user)としている例も見かける(というか、公式の例がそうなっている)。

しかし、create_listとしてしまうとbuild(:user, :user_has_blogs)としたとしても、create(:user, :user_has_blogs)としたのと同じ結果となりDBに保存されてしまう。上記の例のようにすれば、buildメソッドを使ったときにはインスタンスが生成されるだけでDBに保存されることはないし、createメソッドを使ったときにはちゃんとDBに保存される。

テストのパフォーマンスに影響するので、上記のようにbuild_listを利用した方が良い。

ディスクアクセスを最小化する

has_many関連の定義もそうだが、ディスクアクセスが発生すれば、そのぶんパフォーマンスが低下することになる。したがって、ファイルからデータを読み込むようなfactoryは極力インスタンス化させないようにしなければならない。

FactoryBot.define do
  factory :user do
    trait :with_big_data do
      big_data { File.read(Rails.root / 'spec/fixtures/files/user_big_data') }
    end
  end
end

上記のようにtraitを使って、必要な場合にのみ参照させるか、factoryを分けてしまうなどの対応を取った方が良い。