規劃 FactoryBot 小技巧,你的測試可以做得更好

FactoryBot(前身為 FactoryGirl)為知名軟體方案解決公司 ThoughtBot 所開發,其用途非常地單純,就是讓使用者可以輕鬆地生產測試用資料。

而使用 FactoryBot 的方式也相當簡單。只要安裝了 Gem 並新增一個 Factory,就可以產生資料。

Factory 長這個樣子

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

然而雖然 FactoryBot 相當好入門,但是當你在規劃 Factories 的時候,有一些細節必須要注意,否則在未來程式變得複雜化的同時,在測試上恐怕會衍生出許多不必要的麻煩。

  1. 避免指定非必要欄位

    非必要欄位有可能是自動生成的欄位(有 Default 值,例如 created_at),或是不影響資料庫保存的欄位(不需要驗證、可以為 Null 的欄位),倘若這些資料在測試中佔有重要的一環,則使用 trait 來指定。

    舉例來說,假設你有個測試要找出加入超過兩年以上的 User,那麼可以寫成這個樣子:

    FactoryBot.define do
      factory :user do
        first_name "John"
        last_name "doe"
    
        trait :old do 
        joined_at 2.years.ago
        end
      end
    end
    
    最忌諱的就是在限制 Unique 的欄位直接賦值,千萬不要這麼做!因為當你未來要在一個 Test Case 裡面生成兩筆以上的資料的話,就會產生驗證錯誤而無法儲存資料。
    
    # 請不要這麼做
    FactoryBot.define do
      factory :user do
        first_name "John"
        last_name "doe"
        id_number 'E1222333444' #unique column
      end
    end
    
  2. 指定有限制欄位有方法

    倘若我們真的有需要在 Unique 的欄位賦值,有什麼好方法嗎?你會有以下幾種做法。

    • 手動賦值 如果只是偶爾需要賦值的情況下,我會建議直接在生產資料的時候手動賦值,例如 FactoryBot.create(:user, id_number: 'E1222333444'),如此一來比較容易掌握生成的資料。
    • 利用 Sequences 這是 FactoryBot 所提供的一個方法,可以以 auto incremental 的方式產生資料。

      FactoryBot.define do
        factory :user do
          first_name "John"
          last_name "doe"
          # 第一個參數是欄位名稱,第二個參數則是起始的數值。
          sequence(:id_number, '01') { |n| "E12223334#{n}" }
        end
      end
      

    如此一來,每當用 FactoryBot 產生 User 的時候,他就會自動生成不重複的 id_number,會從 E1222333401, E1222333402 這樣的形式來新增。

    不過這樣的做法會有一個問題,假設 id_number 有限制其字元數必定為 11,那麼當我們生產到第一百個 user 的時候,它的數值就會變成 E12223334100,會多出一個字元。為了避免這樣的情況,我們可以使用 Ruby 的 #cycle method,將它改成這樣子: sequence(:id_number, ('01'..'99').cycle) { |n| "E12223334#{n}" }。 如此一來,當生產到第一百個 User 的時候,它就會自動迴圈變回 E12223333401 了。

  3. 善用 Trait 讓你的資料更淺顯易懂 上面有提到利用 trait 來指定非必要欄位,你也可以用 Trait 來讓你的程式碼可讀性更高。舉例來說,我們有一個欄位叫做 status 用來記錄會員的帳號狀態,那麼在定義 Factory 的時候便可以利用 trait 來標示這些狀態。 “`ruby FactoryBot.define do factory :user do firstname "John” lastname “doe” end

    trait :active { status ‘active’ } trait :trial { status ‘trial’ } trait :retired { status ‘retired’ } trait :suspended { status ‘suspended’ } end “`

    這樣的做法不僅讓你的 Factory 變得乾淨,同時當你在生成資料的時候,也可以一目瞭然。特別是當你的測試非常需要各種不同的狀態下變動時,能夠更清楚明白測試的目標和內容。

  4. 搭配 Faker 隨機生產資料 Faker 是對於命名能力缺乏的工程師的一種救贖。它的能力作用就是生產隨機資料,從一般的信用卡卡號和人物的人名,到權力遊戲和 Pokemon 的角色,它都可以幫你生出來,用來瘩配 FactoryBot 生產測試資料,可謂妙不可言。 不過,當使用 Faker 搭配 FactoryBot 的時候,有些事情必須要注意。首先是在寫 Factory 的時候,我們必須用 block 來賦值給欄位,假設我們用參數的方式賦值的話,他只會隨機產生出其中一個結果,意味著如果你的欄位是 unique 的話,產生第二筆以上資料就會發生錯誤。 ”`ruby FactoryBot.define do factory :user do firstname “John” lastname “doe” # 會重複出現一樣的公司名稱 company_name Faker::Company.name end end

    FactoryBot.define do factory :user do firstname “John” lastname “doe” # 無論產生幾筆資料都是隨機的 company_name { Faker::Company.name } end end “`

另外一點則是,有些 Faker 內的 Seed 是有限的,遇到欄位是 Unique 的情況下必須謹慎使用 Faker,否則在建立愈多資料的情況,愈有可能隨機到相同的值造成資料庫的寫入錯誤。因為這種問題有隨機性,也比較難除錯,因此要盡可能避免這樣的狀況,當你發現某個測試常常出現這樣的情況,那就乾脆地手動賦值吧!

## 小結

FactoryBot 是個功能單純而且容易操作的套件,不過針對愈複雜的測試情況和應用也有著相當進階的功能,在這篇文章中提出一些初始規劃 Factory 時應該避免或者注意的地方,希望拋磚引玉能讓更多人知道 FactoryBot 的好處及妙用。