假資料,Faker 以及 FactoryGirl
開發及測試時會需要使用一些假資料,這裡介紹一些建假資料的方法。
Faker
https://github.com/stympy/faker
用來產生像是電話、Email、名稱等假資料:
Faker::Name.name #=> "Christophe Bartell"
Faker::Business.credit_card_number #=> "1228-1221-1221-1431"
Faker::Internet.safe_email #=> "Christophe@example.org"
也可以產生「冰與火之歌」的角色人物:
Faker::GameOfThrones.character #=> "Tyrion Lannister"
Faker::GameOfThrones.house #=> "Stark"
Faker::GameOfThrones.city #=> "Lannisport"
或是精靈寶可夢的名稱:
Faker::Pokemon.name #=> "Pikachu"
Faker::Pokemon.location #=> "Pallet Town"
當然以上這些,都可以自己用 Ruby 寫,不一定要用這個 Gem,用這個 Gem 節省時間,不重複造輪子。
Rails 有內建假資料的機制,叫做 Fixture。
Fixture
Rails 內建的機制,用法如下。
建立一個名稱為 JuanitoFatas
、email 是 [email protected]
的假 User:
# spec/rails_helper.rb
RSpec.configure do |config|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = true
end
# spec/fixtures/users.yml
juanitofatas:
name: JuanitoFatas
email: [email protected]
# 在測試裡使用 users(:juanitofatas) 就會回傳在 users.yml 定義的資料
user = users(:juanitofatas)
# => #<User:0x1234 name: "JuanitoFatas" email: "juanito@jollygoodcode.com">
但這樣是寫死的,沒有彈性,這也是為什麼我們都採用 Factories。
Factories
最流行也是最常見的做法,先定義一個 Factory class,在個別方法裡寫怎麼建各式各樣的 object:
class UserFactory
def self.create_user
User.create(title: "Ryan")
end
def self.create_admin
User.create(title: "Ryan", admin: ture)
end
end
在測試便可以這樣子來建立使用者跟管理員:
UserFactory.create_user
UserFactory.create_admin
這套方法有一個最常用的 Gem,提供了定義 Factory 的 DSL,內建了許多方便好用的機制,滿足建立假資料的各種需求,這個 Gem 就是 FactoryGirl。
FactoryGirl
Factory Girl 當下最新版本為:4.7.0(2016/04/01)
測試一定會需要準備假資料,譬如建一個假的使用者帳號,假的訂單,諸如此類。而 Rails 內建的 Fixtures 機制不夠好用,複雜的場景下無法輕易的滿足需求,所以有了 Factory Girl。Factory Girl 是 thoughtbot 公司維護的 RubyGem,提供了簡單易懂的語法,支援各式各樣產生假資料的複雜情況,簡化了產生假資料的難度。
安裝
Ruby 專案
GitHub: https://github.com/thoughtbot/factory_girl。
group :development, :test do
gem "factory_girl"
end
接著 bundle
安裝即可。
Rails 專案
GitHub: https://github.com/thoughtbot/factory_girl_rails。
group :development, :test do
gem "factory_girl_rails"
end
接著 bundle
安裝即可。
FactoryGirl 基本設定
建立一個新檔案:spec/support/factory_girl.rb
:
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
spec/spec_helper.rb
要記得引入 spec/support/
目錄下的所有文件:
Dir["spec/support/**/*.rb"].each { |f| require f }
有了這個配置以後,本來要輸入:
FactoryGirl.build(:user)
現在在測試裡只需要輸入:
build(:user)
無需使用 FactoryGirl
前綴係因為已經把 FactoryGirl
mixin 進來了。
定義 Factory
每一個 Factory 對應一個 Active Record 的 Model,每個 factory 需要指定名字與屬性。
factory <symbol of class> do
# define attributes here
# attr value 像是這樣:
# name "Juanito"
end
Factory Girl 會根據這個 symbol 自動推斷出對應的類別名稱,比如 :user
對應為 User
。
而 do...end
區塊內則可以指定屬性的預設值。
譬如我們定義一個 Factory 來建出 User
的 instance:
FactoryGirl.define do
factory :user do # <----- Factory 的名字:User
first_name "Anakin" # <----- User 的屬性 name
last_name "Skywalker" # <----- User 的屬性 last_name
admin false # <----- User 的屬性 admin
end
end
使用 Factory
定義了使用者的 Factory 之後:
FactoryGirl.define do
factory :user do
first_name "Anakin"
last_name "Skywalker"
admin false
end
end
該如何使用呢?
user = build(:user)
# 等同於 User.new(...)
user = create(:user)
# 等同於 User.create(...)
attrs = attributes_for(:user)
# 等同於 attrs = { first_name: "Anakin", last_name: "Skywalker", admin: false }
這樣在測試需要建立假使用者的時候,只需要使用這些方法即可。
一個 Model 一個 Factory
推薦每個 Model 擁有自己一個 Factory 定義,而每個 Factory 只提供最基本的資料。譬如只需要提供所有需要通過 validations 的屬性即可,也就是最小合法物件。
最小合法物件
Minimum Valid Object,什麼是“最小合法物件”?譬如:
class User < ActiveRecord::Base
validates_presence_of :first_name
end
比如要建立新使用者,則必須要有名字才可以通過驗證(合法)。
最小合法物件就是最少需要給什麼欄位,才可以建立出新的物件(使用者)。
按照這個原則,不必要的欄位不要在 Factory 裡定義:
FactoryGirl.define do
factory :user do
first_name "Anakin"
end
end
這樣子就可以了。
Factory 檔案擺放位置
Factory Girl 在載入時,會使用 FactoryGirl.find_definitions
方法來找到定義的 Factories,預設的尋找路徑有:
spec/factories.rb
spec/factories/*.rb
可以把所有的 Factory 都定義在 spec/factories.rb
,或是放在 spec/factories/
目錄下按照 Model 的慣例命名。
spec/factories.rb
spec/factories/*.rb # spec/factories/user.rb
搭配 Factory Girl 與 Minitest 使用時,則是會在這裡尋找:test/factories.rb
或 test/factories/*.rb
。
基本用法
假設我們有一個 User Factory:
FactoryGirl.define do
factory :user do
first_name "Anakin"
last_name "Skywalker"
admin false
end
end
則會有這些方法可用:
# User.new
user = build(:user)
# User.create(factory 裡定義的屬性)
user = create(:user)
# 回傳由 factory 裡定義的屬性所組成的 Hash
user_attrs = attributes_for(:user)
# 回傳一個物件,每個方法都回傳值都按照 Factory 所定義的值來造成。
user = build_stubbed(:user)
定義的屬性是可以覆蓋的:
create(:user, first_name: "Luke")
惰求值屬性
將屬性的值包在 { ... }
區塊之內,則可以在需要用到的時候,才進行求值,又稱為懶惰求值。
factory :user do
first_name "Anakin"
date_of_birth { 42.years.ago }
end
first_name
會在 Factory 定義時(RSpec 載入 Factory Girl 時),便進行求值;然而 date_of_birth
只有在用到時,才進行求值。
T> 建議所有的屬性都採用這個方式來定義,這樣載入 Factory 檔案時速度比較快。
相依屬性
比如假資料的某個屬性,利用別的屬性產生,譬如用姓名產生的 Email:
factory :user do
first_name "Anakin"
last_name "Skywalker"
email { "#{first_name}.#{last_name}@example.com".downcase }
end
attributes_for(:user)[:email]
=> "[email protected]"
順序屬性
譬如 Email 需要是不重複的,就可以使用順序屬性。
# Defines a new sequence
FactoryGirl.define do
sequence :email do |n|
"person#{n}@example.com"
end
factory :user do
email # 等同於 email { generate(:email) }
end
end
這樣每次建立 User 時,就會建出遞增的 Email
create(:user)
=> <User email: "[email protected]">
create(:user)
=> <User email: "[email protected]">
關聯
比如文章(post)有單一作者(author),作者是 User。而我們已經定義好了 user
Factory,則 post
Factory 可以這樣定義:
class User
has_many :posts
end
class Post
belongs_to :author, class_name: "User"
end
factory :user do
first_name "Anakin"
end
factory :post do
# ...
association :author, factory: :user
end
在新建文章 create(:post)
時,會自動使用 user
Factory 的預設值來建立作者。
Trait 和繼承
變更屬性的值,使用 Trait 來區分不同種類的使用者。比如一般使用者與管理員使用者:
FactoryGirl.define do
factory :user do
first_name "Anakin"
last_name "Skywalker"
trait :with_admin do
admin true
end
end
end
用法
user = create(:user)
admin_user = create(:user, :with_admin)
也可以使用繼承:
FactoryGirl.define do
factory :user do
first_name "Anakin"
last_name "Skywalker"
end
factory :admin_user, parent: :user do
admin true
end
end
個人偏好用 trait 來組出想要的 Factory,遵循 Composition instead of Inheritance 原則。
一次建多個物件
create_list(:post, 2)
等同於
2.times do
create(:post)
end
也可以覆蓋屬性:
create_list(:post, 2, created_at: 7.days.ago)
同樣有 build_list
來建出尚未儲存的物件。
has_many 關聯
要怎麼樣在建立使用者時,順便建立幾篇文章呢?
factory :user do
first_name "Anakin"
trait :with_posts do
after(:create) do |user, evaluator|
user.posts << create_list(:post, evaluator.posts_count)
end
end
end
這樣使用 create(:user, :with_posts)
建立使用者時,會同時建出兩篇文章,也可以透過覆蓋 posts_count
來建立更多篇文章:
create(:user).posts.length # 0
create(:user, :with_posts).posts.length # 2
create(:user, :with_posts, posts_count: 5).posts.length # 5
除了 Factory Girl 之外的選擇
譬如 discourse/discourse 採用的是 Fabrication,大同小異。
官方文件
以上介紹了常見的 FactoryGirl 使用方法,完整使用方法請查閱官方教學文件:https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md。
FactoryGirl.lint
可以檢查所有的 Factory 是否正確定義,可以在 rails console
下輸入 FactoryGirl.lint
來試試。