除了共用案例和自訂匹配器之外,我們可以像 Rails 的 View Helper 一樣定義輔助方法,假設我們的測試需要做非常多的準備,利用自訂輔助方法可以幫我們極大的改善測試案例的可讀性。
使用方式
因為 RSpec 是透過 DSL 來實現的,因此我們的測試本身也是一個 Ruby 物件,要定義輔助方法可以直接在測試中定義。
1RSpec.describe Order do
2 # ...
3 def there_have_some_products_in_order(product_ids:)
4 product_ids.each do |id|
5 order.add_product!(product_id: id)
6 end
7 end
8
9 subject(:order) { create(:order) }
10
11 before do
12 there_have_some_products [
13 { id: 1, name: 'PS5', price: 15000 },
14 { id: 2, name: 'Switch', price: 9000 }
15 ]
16 there_have_some_products_in_order(product_ids: [1, 2])
17 end
18
19 it { is_expected.to have_attributes(subtotal: 24000) }
20end
原本我們在撰寫測時,會在 before
上將建立商品、訂單內品項的實作直接寫上去,然而這個情況會讓我們的測試在不同的區段有非常多的「重複」並且佔據非常大面積的測試內容,除了閱讀不方便之外,也很能讓人馬上理解「這裡有一些商品,而且都在訂單中」這樣的資訊。
透過輔助方法,我們就可以用 there_have_some_products
這樣的方式去呈現更加直觀的測試資訊。
使用模組
直接在測試中定義雖然方便,然而我們經常會有不同的測試會要用到類似的行為,這個時候抽離出來變成模組,就可以統一整個團隊在特定「動作」的行為,並且有著統一的方式去描述測試的行為。
1module ProductHelper
2 def there_have_some_products(*products)
3 @products = products.map do |attributes|
4 create(:product, **attributes)
5 end
6 end
7end
8
9module OrderItemHelper
10 def there_have_some_products_in_order(*product_names)
11 product_names.each do |name|
12 order.add_product(@products[name])
13 end
14 end
15end
使用時,就可以直接用 include
加入到測試裡面。
1RSpec.describe Order do
2 include ProductHelper
3 include OrderItemHelper
4
5 subject(:order) { create(:order) }
6
7 before do
8 there_have_some_products [
9 { id: 1, name: 'PS5', price: 15000 },
10 { id: 2, name: 'Switch', price: 9000 }
11 ]
12 there_have_some_products_in_order('PS5', 'Switch')
13 end
14
15 it { is_expected.to have_attributes(subtotal: 24000) }
16end
在這個案例中,我們可以適度的利用實例變數(Instance Variable)來儲存資訊輔助我們的輔助方法更加可讀,然而要注意實例變數可能會造成一些副作用(Side Effect)因此在使用上需要格外小心。
自動載入
除了手動引用模組來載入輔助方法之外,我們也可以善用 RSpec 的後設資料( Metada)來處理何時該載入哪些模組。
1RSpec.configure do |config|
2 # ...
3 config.include ProductHelper, feature: :order
4 config.include OrderItemHelper, feature: :order
5end
此時只需要在測試中加上 feature: :order
就能夠自動的使用到這些方法。
1RSpec.describe Order, feature: :order do
2 subject(:order) { create(:order) }
3
4 before do
5 there_have_some_products [
6 { id: 1, name: 'PS5', price: 15000 },
7 { id: 2, name: 'Switch', price: 9000 }
8 ]
9 there_have_some_products_in_order('PS5', 'Switch')
10 end
11
12 it { is_expected.to have_attributes(subtotal: 24000) }
13end
如果希望要加入到所有測試中,則可以跳過 feature: :order
的設定,在 Rails 的測試中,大多會有像是 type: :model
這類後設資料,就是為了對應不同的測試情境,用來選定該載入哪些輔助方法。
當測試的數量變多時,RSpec 更建議自己用
require
載入輔助模組來處理,而不是透過自動載入所有檔案的方式,像這樣減少需要載入的檔案可以加入測試的運行。
擴充測試
除了引用之後讓 RSpec 的測試案例可以使用,我們也可以使用擴充的方式讓我們更容易進行一些測試情境的定義,然而這個機制存在一定的限制跟缺點,應該要視情況去使用會比較好。
1module SessionHelper
2 def login_as(type)
3 before do
4 token = generate_auth_token_from(send(type))
5 request.headers['Authorization'] = "Bearer #{token}"
6 end
7 end
8end
1RSpec.describe MyAPI do
2 include SessionHelper
3
4 let(:user) { create(:user) }
5 login_as(:user)
6
7 describe 'GET /' do
8 subject { get '/' }
9
10 it { is_expected.to be_successful }
11 end
12end
雖然可以像是 let
這樣使用,然而同樣的會受到一些限制。舉例來說我們就無法用 login_as(user)
這樣的寫法,而需要用 login_as(:user)
再用 send(type)
來取得我們用 let
定義的 user
物件,相比之下,直接定義成可以在 before
中使用的方法更為實用。
也因此,會需要使用到擴充方式定義的輔助方法,我們可以預期是少數用來定義或者設定測試的情境,跟實際測試通常關聯會比較小。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用優雅的 RSpec 測試回饋表單告訴我。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 優雅的 RSpec 測試 - 前言
- 優雅的 RSpec 測試 - 撰寫測試的方式
- 優雅的 RSpec 測試 - RSpec 概觀
- 優雅的 RSpec 測試 - 測試案例
- 優雅的 RSpec 測試 - 組織測試
- 優雅的 RSpec 測試 - 前置處理
- 優雅的 RSpec 測試 - 常見匹配器
- 優雅的 RSpec 測試 - 內容匹配
- 優雅的 RSpec 測試 - 錯誤匹配
- 優雅的 RSpec 測試 - 共用案例
- 優雅的 RSpec 測試 - 自訂匹配器
- 優雅的 RSpec 測試 - 輔助方法
- 優雅的 RSpec 測試 - 測試替身
- 優雅的 RSpec 測試 - Mock 與 Stub
- 優雅的 RSpec 測試 - Allow 的使用方式
- 優雅的 RSpec 測試 - Allow 變化應用
- 優雅的 RSpec 測試 - Spy 的應用
- 優雅的 RSpec 測試 - 物件的可測試性
- 優雅的 RSpec 測試 - 耦合與依賴注入
- 優雅的 RSpec 測試 - 探索式的測試與重構