技術書典は書く側で参加したい気持ちはあるけど、書くネタと書く時間があるかどうか…
— 神速@リリカルエンジニア (@sinsoku_listy) 2017年4月9日
あー、自分の知ってるRailsアンチパターンとか書きたいかも。自分の犯した罪(アンチパターン)を贖罪したい…。
— 神速@リリカルエンジニア (@sinsoku_listy) 2017年4月9日
技術書典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:無駄なレコードを生成するとテストが遅くなってしまいます。