蒼時弦也
蒼時弦也
資深軟體工程師
發表於

優雅的 RSpec 測試 - Allow 的使用方式

在 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 測試回饋表單告訴我。