在 RSpec 的測試中,我們最常會使用到的是 Allow 來改變一個物件的回傳,基本上在需要使用測試替身(Test Double)的地方,就會使用用到。
使用方式
要使用 Allow 的方法很簡單,我們只需要定義想要改變的物件以及當哪個方法被呼叫時會回傳什麼即可。
1RSpec.describe Order do
2 let(:product) { build(:product) }
3
4 before do
5 allow(product).to receive(:price).and_return(100)
6 end
7
8 # ...
9end
假設我們希望根據不同的參數來回傳不同的內容,則可以改為使用 Block 的方式定義。
1RSpec.describe Product do
2 subject(:product) { build(:product) }
3
4 before do
5 allow(product).to receive(:variants) do |type|
6 if type == :color
7 [build(:product, :with_red_color)]
8 else
9 []
10 end
11 end
12 end
13
14 # ...
15
16 describe '#variants' do
17 subject { product.variants(type) }
18
19 let(:type) { nil }
20
21 it { is_expected.to be_empty }
22
23 context 'when type is color' do
24 let(:type) { :color }
25
26 it { is_expected.not_to be_empty }
27 end
28 end
29end
像這樣我們就可以根據參數控制回傳或者進行運算,然而這些方式都可能影響測試的正確性,在使用上來說應該更加小心。
除此之外,我們可以用 #with
來篩選不同參數該回傳的數值。
1RSpec.describe Product do
2 subject(:product) { build(:product) }
3
4 before do
5 allow(:product).to receive(:variants).and_return([])
6 allow(:product).to receive(:variants).with(:color).and_return([build(:product, :with_red_color)])
7 end
8
9 # ...
10end
像這樣我們就能夠根據不同的情況指定預期會回傳的數值。
假設我們希望「模糊」的去比對傳入的參數,那麼也可以利用「匹配器」來實現。
1RSpec.describe AccountHolder do
2 let(:account) { build(:bank_account) }
3
4 before do
5 # 前面示範的 have_amount 匹配器
6 allow(account).to receive(:deposit).with(have_amount(100))
7 end
8
9 # ...
10end
透過匹配器,我們就可以善用像是 include
之類的匹配來判斷檔名、物件類型等等不同的參數,就可以簡單地讓特定行為固定回傳某些結果。
假設我們希望拋出錯誤,則可以使用 #and_raise
來取代 #and_return
進行處理。
如果沒有給
#and_return
的情況預設回傳nil
1RSpec.describe UserCreator do
2 let(:repo) { UserRepository.new }
3
4 before do
5 allow(repo).to receive(:find!).and_raise(UserNotFound)
6 end
7
8 # ...
9end
善用這些替代方法的機制,我們基本上就能夠處理大多數測試的情況。
Allow Any Instance Of
跟 Allow 一樣,經常被使用的還有 allow_any_instance_of
這個用法,它可以讓我們「一次性」的對某個類別的實例(Instance)進行 Stub 的處理,然而 Rubocop 並不推薦。
這是因為當我們需要像這樣處理時,就表示我們在 Dependency Injection(依賴注入)的地方沒有很好的處理造成耦合,因此需要透過這種方式來解決。
舉例來說,我們有一個像這樣的物件。
1class UserCreator
2 def initialize
3 @repo = UserRepository.new
4 end
5
6 def verify_email(user_id)
7 user = @repo.find(user_id)
8 RegistrationMailer.verify(user).deliver_now
9 end
10
11 # ...
12end
當我們想要讓 UserRepository#find
直接回傳特定使用者來驗證 UserCreator#verify_email
有正確的動作時,就會因為無法製作替身而需要使用 allow_any_instance_of
來處理。
如果改為使用依賴注入的方式,只需要調整為
1class UserCreator
2 def initialize(repo)
3 @repo = repo
4 end
5
6 # ...
7end
這樣在撰寫測試時,就可以將「替身」注入到裡面,一方面讓物件的可測試性提升,另外一方面也幫助我們降低了耦合度,關於這部分的討論會在後面的文章詳細說明。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用優雅的 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 測試 - 探索式的測試與重構