Fabricationを使ってみた

※2011/11/08 コメント欄で指摘を頂いた箇所を加筆修正しました。また、割と古い記事ですので最新の情報は Fabrication を参照することをお奨めします。

これを作ってるとき、machinistとmachinist_mongoを使っていたんですが、試しに前々から気になっていたFabricationで書き換えてみました。README書いてあることをいくつか実際にやってみたのでメモしておきます。割と使いやすかったです。

何に使えるの

下記をサポートしてるそうですので、Mongoid使えます。やった!

使ったもの

  • Fabrication…本日のメイン
  • Faker…嘘データをどんどこ作ってくれるやつ

QuickStart & 使い方

Gemfile

Rails/Mongoid/RSpec/Fabricationで↓のような組み合わせで使ってます。

group :development, :test do
  gem 'rspec-rails', '>= 2.0.0.beta.22'
  gem 'mongoid-rspec'
  gem 'faker'
  gem 'fabrication'  # <= これ
  gem 'wirble'
  gem 'spork'
end
記述する場所

spec/fabricators.rbに書くか、spec/fabricators/*.rbに複数ファイルにして書いてもいいみたいです。ここに置いておけば、必要になったら勝手に呼ばれます。最初から自分で読み込んでおきたいときはspec/spec_helper.rbなどに書きます。

Fabricatorを書いて使ってみる

spec/fabricators.rbなどにfabricatorを書いていきます。blueprintみたいな感じで。

# Userモデルはこんな感じだとします
class User
  include Mongoid::Document
  field :screen_name, :type=>String
  field :description, :type=>String
  field :site, :type=>String
  field :gender, :type=>String
end

# Userモデルのfabricatorを作ります
Fabricator(:user) do
  screen_name { Faker::Internet.user_name }
  description { Faker::Lorem.paragraph[0..190] }
  site { ['http://', Faker::Internet.domain_name].join }
  gender { ['male', 'female'].sort_by{ rand }.first }
end

作ったFabricatorをspecの中で好きなときに呼び出します。factorygirlやmachinistと同じように、3通りの呼び出し方があります。

# インスタンスを保存して返す
Fabricate(:user)
# => #<User _id: 4cb18e0c011c0e5a2c000001, screen_name: "aidan", description: "Vel iste illo beatae maxime earum accusantium. Facilis doloremque nemo dolores perspiciatis. Eum et nisi sunt qui soluta facilis consequatur quasi. Qui sunt omnis beatae sit. Quisquam earum d", site: "http://wintheiserwunsch.com", gender: "female">

# インスタンスを保存せずに返す
Fabricate.build(:user)
# => #<User _id: 4cb18e32011c0e5a2c000002, screen_name: "darlene", description: "Exercitationem voluptatem voluptatibus officia saepe labore earum. Voluptas ut ipsam soluta. Libero possimus eius illo soluta quo blanditiis quod. Similique ea qui reprehenderit nihil ut cons", site: "http://schiller.uk", gender: "female">

# 属性だけを生成してHashで返す
Fabricate.attributes_for(:user)
# => {"screen_name"=>"jaylon", "description"=>"Et vitae nobis enim non. Quia sunt ipsum laboriosam. Maxime ut neque voluptatum velit est aliquam sequi qui. Eius dolorem id ut assumenda.", "site"=>"http://wuckert.uk", "gender"=>"male"}
Fabricatorの継承とか

すでに作ったのとはちょっと違うのがほしいときは、すでにあるfabricatorを継承することができます。:fromパラメータに継承元の名前を入れて、変更したいフィールドの定義を記述します。

# 必ずfemaleなひとがほしい
Fabricator(:female, :from=>:user) do
  gender 'female'
end

もちろん、継承は使わず呼び出すときに直接指定することもできます。

# いまだけfemaleなひとがほしい
Fabricate(:user, :gender=>'female')
# => {"screen_name"=>"daron", "description"=>"Aut dolor reprehenderit consequuntur nihil magni odit eveniet. A est magni nam vero praesentium. Praesentium quae perferendis mollitia. Voluptatem harum ex aperiam tempora ab nihil voluptas. ", "site"=>"http://huel.com", "gender"=>"female"}
クラスの明示的な指定

:class_nameパラメータで明示的にクラス名を指定することもできます。色々なパターンでFabricatorをつくりたいときなどはこれで。クラス名はシンボルやklass、文字列で指定できます。

# :class_name は :user, User, 'User' などで指定可
Fabricator(:invalid_user, :class_name=>:user) do
  screen_name 'hibariya'
  description 'i have no gender'
end
Sequenceで一意にする

MachinistのShamのようなものも用意されています。*1Fabricate.sequenceを使うと、呼び出すたびにインクリメントする数値が得られます。ブロックも渡せるので数値以外の一意な何かもつくることができます。Fakerには大量の単語とかがコード中にドカっと入ってるんですが、普通に重複しますので、データが一意でないといけないときなどはこれを使います。
大きく分けると3つの書き方になるようです。

Fabricate.sequence(:foo_id) # 呼び出すたびに0から始まる連番を返す
Fabricate.sequence(:bar_id, 1000) # 呼び出すたびに1000から始まる連番を返す
Fabricate.sequence(:future){|n| n.days.since } # 呼び出すたびに1日ずつ未来の日付を返す

さっきのUserモデルの例だとこんな感じになります。

# uriもscreen_nameも重複させたくない!
Fabricator(:user) do
  screen_name { Fabricate.sequence(:screen_name){|i| [Faker::Internet.user_name, i.to_s].join } } # <= ここ
  description { Faker::Lorem.paragraph[0..190] }
  site { Fabricate.sequence(:uri){|i| ['http://', i.to_s, Faker::Internet.domain_name].join } } # <= ここ
  gender { %w(male female).sort_by{ rand }.first }
end
on_init, init_with でnewに引数を渡す

newするのに引数が必要なクラスのfabricatorを作るときなどに使えます

class Site
  def initialize(uri) @uri = uri end
end

# on_initとinit_withで、initializeに渡す引数を指定できる
Fabricator(:site) do
  on_init{ init_with(['http://', Faker::Internet.domain_name].join) }
end
after_build, after_create コールバック

保存する前、保存したあとのコールバック。さっきまで勘違いしていたんですが、Fabricate.buildでもafter_createは呼ばれるようです(でもsaveはされない)これはバグだったそうです(https://github.com/paulelliott/fabrication/pull/36
)。

# User#fetch_recent_entries!がsave前のステップで呼び出される
Fabricator(:foo_user, :from=>:user) do
  after_build {|user| user.fetch_recent_entries! }
end

# User#fetch_recent_entries!がsave後のステップで呼び出される
Fabricator(:bar_user, :from=>:user) do
  after_create {|user| user.fetch_recent_entries! }
end
belongs_to, references_oneな関連を!で表現

belongs_to, references_oneな関係にあるものは!を付けると自動的に作ってくれるらしい。べんり!というのは間違いで、!を付けるか否かで関連が作られるタイミングが変えられるようです(!を付けなければ参照されるまで作られない)。

Fabricator(:umbrella) do
  color { %w(red green blue pink violet purple).sort_by{ rand }.last }
end

Fabricator(:girl) do
  name 'unknown'
  umbrella! # !を付けると即座に関連が生成される
end

Machinist <=> Fabrication 比較表

Machinistとの違いを、よく使う項目だけ表にしてみるとこんな感じでしょうか。*2

Machinist Fabrication
Model.blueprint do ~ end Fabricator(:model) do ~ end
Model.blueprint(:named_blueprint) do ~ end Fabricator(:named_fabricator, :from=>:model) do ~ end
Model.make Fabricate(:model)
Model.make_unsaved Fabricate.build(:model)
Model.plan Fabricate.attribues_for(:model)

*1:Shamを使うと呼び出すデータの順番や一意性が保証されるのでした

*2:注) Model, :modelは何かのモデルクラスに読み替えます