読者です 読者をやめる 読者になる 読者になる

Rails アンチパターン - 錆びついたファクトリー (factory_girl)

技術書典2 に行ったら無性に本を書きたくなったけど、本書くのは 面倒 大変です。

というわけで、とりあえずブログに記事を1つ書いてみた。

factory_girl

factory_girl はテスト用データを作成するときに使う gem です。

下記は User のモデルを定義するファクトリーです。

FactoryGirl.define do
  factory :user do
    first_name "John"
    last_name  "Doe"
    admin false
  end
end

このファクトリーから User のテストデータを生成することができます。

FactoryGirl.create(:user)
#=> #<User id: 1, first_name: "John", ...

factory_girl は関連先のレコードを自動生成したり、連番を生成したり、結構多機能だったりします。

このあたりの詳細を知りたい方は公式の README を読むか、英語が苦手なら日本語の紹介記事などを読むと良いです。

錆びついたファクトリー

ここからが本題で、factory_girl で定義したファクトリーは時間が経ったり、作成者が未熟だと 錆びる (=ビルドしづらい、できなくなる)ことがあります。

錆びついたファクトリー例を紹介してみたいと思います。

2回以上ビルドできない

class User
  validates :nickname, uniqueness: true
end

FactoryGirl.define do
  factory :user do
    nickname "sinsoku"
  end
end

これはひどい。 uniqueness 制約により2回目のビルドは失敗します。

FactoryGirl.define do
  factory :user do
    sequence(:nickname) { |n| "nickname_#{n}" }
  end
end

上記のように sequence を使って回避すべきです。

build_stubbed なのにレコードが生成される

クラスの属性だけでテストができる( DB アクセスが不要な)ケースがあります。

class Book
  def published?
    published_at <= Time.current
  end
end

このようなメソッドのテストでは DB アクセスをしないように build_stubbed を使うべきです。*1

しかし、ファクトリーが下記のような定義だとレコードが生成されてしまいます。

FactoryGirl.define do
  factory :book do
    author { create(:user) }
    published_at '2017-04-10'
  end
end

これは association を使って定義すべきです。

FactoryGirl.define do
  factory :book do
    association :author, factory: :user
    published_at '2017-04-10'
  end
end

association を使うことで、 build_stubbed の時にレコードが生成されなくなります。

デフォルトのファクトリーが重い

FactoryGirl.define do
  factory :user do
    name 'serval'

    transient do
      friends_count 10
    end

    after(:create) do |_user, evaluator|
      create_list(:user, evaluator.friends_count)
    end
  end
end

デフォルト値のフレンズが多すぎます><

FactoryGirl.define do
  factory :user do
    name 'serval'

    factory :user_with_friends do
      transient do
        friends_count 10
      end

      after(:create) do |_user, evaluator|
        create_list(:user, evaluator.friends_count)
      end
    end
  end
end

デフォルトのファクトリーはシンプルに保ち、フレンズの多いファクトリーは別にしておきましょう。

条件の必要なファクトリー

class Servant
  belongs_to :master

  validate :must_be_valid_master

  TEAM = {
    'Artoria Pendragon' => 'Shirou Emiya'
  }

  private

  def must_be_valid_master
    errors.add(:master, ' is invalid') unless TEAM[name] == master.name
  end
end

FactoryGirl.define do
  factory :servant do
    association :master, factory: :user
    name 'Artoria Pendragon'
  end
end

このファクトリーは master が ‘Shirou Emiya’ のときだけビルドできます。

user = FactoryGirl.create(:user, name: 'Shirou Emiya')
servant = FactoryGirl.create(:servant, master: user)

このように、ビルド時に必要な関連を渡すようなファクトリーは使いづらいです。

FactoryGirl.define do
  factory :servant do
    association :master, factory: :user, name: 'Shirou Emiya'
    name 'Artoria Pendragon'
  end
end

FactoryGirl.create(:servant) だけでビルドが出来るようにしておくべきです。

その他

時間の経過によりバリデーションが追加・変更されたり、モデルの関連が変わってしまい、ファクトリーが錆びてしまうことがあります。

アンチパターンへの対策

幸い factory_girl には Lint がついています。

# lib/tasks/factory_girl.rake
namespace :factory_girl do
  desc "Verify that all FactoryGirl factories are valid"
  task lint: :environment do
    if Rails.env.test?
      begin
        DatabaseCleaner.start
        FactoryGirl.lint
      ensure
        DatabaseCleaner.clean
      end
    else
      system("bundle exec rake factory_girl:lint RAILS_ENV='test'")
    end
  end
end

上記のような rake タスクを作成し、 CI で常にファクトリーがビルドできるかチェックしておくのが良いでしょう。

おわり

パッと思い出せた factory_girl に関するアンチパターンを書いてみました。

他のアンチパターンはやる気と時間があれば書く…かも?

*1:無駄なレコードを生成するとテストが遅くなってしまいます。