Website logo

Robert Chang

技術部落格

RSpec - 介紹 FactoryBot Rails 以及如何設定

安裝 factory_bot_rails

和 RSpec 一樣,我們把 factory_bot_rails 放在 Gemfile:

group :development, :test do
  gem 'factory_bot_rails', '~> 6.2'
end

記得,在實作專案的時候都要記得放上版本號喔!

factory_bot_rails 本身其實會根據以下的路徑自動載入:

factories.rb
test/factories.rb
spec/factories.rb
factories/*.rb
test/factories/*.rb
spec/factories/*.rb

但若是你有自己想要放置的檔案位置的話呢?

你可以在 config/application.rb 或是 config/environments.rb 中加入以下的指令:

config.factory_bot.definition_file_paths = ["你的資料夾名稱/factories"]

factory_bot_rails 在開發環境時會自動地幫你產生工廠來產生物件,若是你覺得很煩,你想要自己建立檔案的話呢?

你可以把 factory_bot_rails 移出 :development 的 group 或是在設定中加入:

config.generators do |g|
  g.factory_bot false
end

接著我們還有一些基本的設定要做,可以開一個資料夾,路徑是 spec/support/factory_bot.rb 然後在裡面放入這段程式碼,並確認他會被 rails_helper.rb 給 require

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

這段的目的應該不難理解,就是 include FactoryBot 的方法,讓我們可以在 _spec.rb 的檔案中寫 FactoryBot 的方法!

製造工廠

FactoryBot 最大的目的就是製造假的實體讓我們的測試可以通過,若是測試也需要把資料都存進資料庫的話,製作專案的過程中就會浪費很多的硬碟空間,這件事情是沒有必要的。

後面也會提到安裝 Database_cleaner 確保在每次測試前都清空資料庫,讓測試環境都是乾淨的。

這邊先示範一個最基本的製造工廠的程式碼:

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

我們從第二行開始看, factory :user 的意思是,User 類別的工廠,這個 Model 有三種屬性,分別是 first_name 以及 last_nameadmin

這樣就可以在寫測試的時候使用:

build(:user) / create(:user)
# 這樣就可以建立 user 實體,也可以把它賦予在變數裡面
robert = build(:user)
# 這樣他本身就會有我們在工廠裡面設定好的屬性!
robert.first_name # "John"

也可以用不同的命名,但使用同樣的類別:

FactoryBot.define do
  factory :admin, class: 'User' do
    first_name { "John" }
    last_name  { "Doe" }
    admin { false }
  end
end

這樣的話就可以這樣寫:

build(:admin)

Sequence

除了剛剛提到基本屬性的製造外,有一個案例是某些屬性會有不能重複的特性,像是 email 這樣子的屬性!

這時候就可以使用 sequence 來製作獨特的屬性,使用方法像是:

FactoryBot.define do
  factory :user do
    first_name { "John" }
    last_name  { "Doe" }
    admin { false }
    email { generate(:email) }

    sequence(:email) { |n| "person#{n}@example.com" }
  end
end

接著可以製造幾個實體並且印出來看看!

person_1 = build(:user)
person_2 = build(:user)

person_1.email # "person1@example.com"
person_2.email # "person2@example.com"

那個 n 會自動地幫我們遞增,也順便讓這個值成為 unique,當然 sequence 有很多的特性和縮寫,短短一篇文章也很難交代完整。

所以盡量以基本的使用方法來介紹。

build & create 的不同

這兩種方式都可以製造出物件的實體,但最大的不同就在於有沒有被 save,那至於有沒有 save 有什麼關係嗎?

有的,如果沒有 save 的話,在 Feature test 就會找不到畫面的顯示,因為該物件根本沒有被儲存,所以不會出現在畫面上。

其實只要想像到底會不會需要物件的 id 或是關聯需要 id 的?

如果需要,請用 create 來製造物件!

若是我想要一次製造很多的物件呢?

FactoryBot 也有提供了 create_list / build_list 的方法,可以這樣使用:

create_list(:user, 3) # save 3 個 user
build_list(:user, 3)

至於後面的數字就是你需要幾個的意思!

Association

各種不同的實體常常會需要很多的關聯,在測試時也不例外。

一開始在學習使用 FactoryBot 的時候感到很複雜,也很困擾,但搞懂之後就覺得沒有那麼困難。

我們先假設一間商店有很多的漢堡,這時候的工廠該怎麼寫呢?

FactoryBot.define do
  factory :burger do
    association :store, factory: :store
  end
end

# 也可以縮寫成這樣

FactoryBot.define do
  factory :burger do
    store
  end
end

首先是 association :store 這件事,就是建立關聯,接著告訴 FactoryBot 去哪找這個關聯,指向 factory: :store 這間工廠!

這樣寫的關聯就是 Store has_many :burgers 的意思,因為身上會有 store_id 的表格應該是 Burger 才對!

After Create Hooks

今天的情境是,我們想要一間商店在 create 之後,就自動擁有 3 個漢堡。

一般來說在測試裡面,你可能會這樣寫:

let(:store) { create(:store) }

before do
  create(:burger, store: store)
  create(:burger, store: store)
  create(:burger, store: store)
end

OK,這沒有什麼問題,也可以達到你的目的,但當測試檔案變大的時候,就會提升閱讀的舒適度,因為太多 before do end 的情形了。

所以希望這個 store 在建立起來的時候就有漢堡了!

可以在工廠裡面先訂立好規則:

FactoryBot.define do
  factory :store do
    ...

    after :create do |store|
      create_list(:burger, 3,  store: store)
    end
  end
end

這段 Code 的意思就是在 create 的瞬間幫我產生 3 個漢堡~

這樣我們就可以讓 _spec.rb 的檔案很乾淨,很清晰!

Traits

這是我覺得超級好用的功能,可以讓你幫同一個物件貼上不同的標籤。

什麼意思呢?

假設我們的 User 有不同的角色,有管理員,有一般人,這時候原本的我會這樣寫:

robert = create(:user)
robert.role = "Admin"
....

利用後來覆寫的方式來改變狀態,但其實可以不用這樣做,有 trait 這個方法讓我們使用!

一樣在工廠裡面做好設定:

FactoryBot.define do
  factory :user do
    ...

    traits :admin do
      role { 'Admin' }
    end
  end
end

可以想像在工廠裡面已經有一張貼紙叫做 :admin,接著我們可以這樣用:

robert = create(:user, :admin)
robert.role # 'Admin'

超級好用的,這樣就可以把各式各樣的 traits 先建立起來,用於應付各式各樣的測試環境!

安裝 DatabaseCleaner

剛剛有提過,我們希望在每次測試的開始前都有乾淨的環境可以測試,所以我要在環境中加入這個 Gem。

依照手冊來安裝:

# Gemfile
group :test do
  gem 'database_cleaner-active_record', '~> 2.0', '>= 2.0.1'
end

一樣在 spec_helper.rb 中加入:

RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

可以看到它在每一次的測時前都會執行這些指令唷!

關於 transaction 以及 truncation 在這個套件的差別:

transaction: rollback 的方式

truncation: 清除資料庫的所有資料

結語

FactoryBot 的使用方式實在是太多了,就像寫測試一樣,你問一百個人會有一百種寫法。

真的沒辦法一一詳細介紹到每個方法,但這些方法對我來說就很夠用了,甚至你也可以使用 traits 搭配 after_create hooks 來製造標籤以達到很多種不一樣的效果。

如果真的很閒可以看 官方文件,但我自己都是想到實作某種方式才去查我需要的東西。

明天會介紹另一個也很有趣的套件 Capybara,用於瀏覽器測試 ( 終端測試 )。

上一篇文章RSpec - 結合 Ruby on Rails 的介紹

下一篇文章RSpec - 介紹 Capybara 以及如何設定