假資料,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.rbtest/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 來試試。

results matching ""

    No results matching ""