我們已經知道在 RSpec 中有強大的匹配器可以使用,除此之外也可以將經常重複的測試案例設計成共用案例來使用,那麼自訂匹配器則是用來針對我們系統中物件常見的回傳來處理。
使用方式
大多數情況下內建的匹配器足以滿足我們的需求,然而當我們的系統中出現 Value Object(數值物件)的時候,就會出現難以用內建的匹配器處理的狀況。
以電商網站常見的「貨幣」來作為例子,我們在沒有自訂匹配器的狀況下需要像這樣判斷。
1RSpec.describe Cart do
2 # ...
3 describe '#subtotal' do
4 # ...
5
6 # By Attributes
7 it { is_expected.to have_attributes(currency: 'TWD') }
8 it { is_expected.to have_attributes(amount: 1000) }
9
10 # By Object
11 it { is_expected.to eq(Money.new('TWD', 10000)) }
12 end
13end
這類情況在針對比較單純的物件還算容易處理,然而遇到比較複雜的物件(例如:HTTP 回應)就不一定那麼方便,使用共用案例針對屬性驗證,似乎也不太符合「語境」這時就可以利用自訂匹配器來處理。
1RSpec.describe Cart do
2 matcher :be_money do |amount, currency: 'USD'|
3 match { |actual| actual.currency == currency && actual.amount == amount }
4 end
5 # ...
6 describe '#subtotal' do
7 # ...
8
9 it { is_expected.to be_money(1000, currency: 'TWD') }
10 end
11end
透過自訂匹配器的機制,像是 Capybara 就能夠針對整個解析過的 HTML 物件,使用 have_selector('.bg-red-500')
這樣的匹配器來找到一個複雜物件中符合條件的情況。
流暢介面
有時候單一個匹配器可能不足以解決我們的需求,以我們作為例子的 be_money(1000, currency: 'TWD')
在閱讀上其實是不太直覺,因此 RSpec 也提供了我們快速定義 Chain Method 的方式。
1RSpec.describe Cart do
2 matcher :have_amount do |amount|
3 match do |actual|
4 res = actual.amount == amount
5 next res if @currency.nil?
6
7 res && (actual.currency == (@currency || 'USD'))
8 end
9
10 chain :and_currency do |currency|
11 @currency = currency
12 end
13 end
14 # ...
15
16 describe '#subtotal' do
17 # ...
18
19 it { is_expected.to have_amount(1000) }
20 it { is_expected.to have_amount(1000).and_currency('TWD') }
21 end
22end
雖然匹配器的定義變的複雜,但我們也可以觀察到匹配器的使用更加彈性,可以針對情境決定是否要增加檢查幣種。
全域定義
我們前面的例子都是針對特定測試定義,然而更實用的情境是針對 RSpec 進行定義這類專案特有的匹配器。
1RSpec::Matchers.define :have_amount do |amount|
2 match do |actual|
3 res = actual.amount == amount
4 next res if @currency.nil?
5
6 res && (actual.currency == (@currency || 'USD'))
7 end
8
9 chain :and_currency do |currency|
10 @currency = currency
11 end
12end
基本上我們只需要改為使用 RSpec::Matchers.define
來定義匹配器即可,同時如果這個匹配器只適用在某些情況,像是 API 相關的測試,我們還可以定義成模組,只在特定的類型進行啟用。
1module APIMatcher
2 extend RSpec::Matchers::DSL
3
4 matcher :be_valid_response do
5 match do |actual|
6 actual[:code] == '00'
7 end
8 end
9end
假設我們的 API 成功時都會回傳類似這樣的 JSON 結構 { "code": "00", ... }
因此可以定義一個 be_valid_response
來當作匹配器,此時在 RSpec 中可以像這樣使用。
1RSpec.describe 'User API', type: :api do
2 include APIMatcher
3
4 # ...
5 describe 'GET /users' do
6 # ...
7 it { is_expected.to be_valid_response }
8 end
9end
或者在 RSpec 設定直接定義要自動引用這個模組。
1# spec/spec_helper.rb
2
3RSpec.configure do |config|
4 # ...
5 config.include APIMatcher, type: :api
6end
這樣在所有設定 type: :api
的測試中,都可以直接使用這個類型的匹配器。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用優雅的 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 測試 - 探索式的測試與重構