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

優雅的 RSpec 測試 - Allow 變化應用

這篇文章是 優雅的 RSpec 測試 系列的一部分。

我們已經瞭解到該如何利用 Allow 來改變一個物件的回傳,然而在我們使用 Allow 的時候還可能會遇到需要呼叫多次、搭配 Block 使用的情況,在這樣的狀況下該如何處理呢?

Each 的替身

在 Ruby 中如果是一個看起來像是陣列可以列舉(Enumerable)的物件,我們會很自然的使用 #each 方法來處理,然而當我們希望被呼叫的對象回傳不同的內容,是否有辦法製作對應的替身呢?

假設我們有一個叫做 MovieList 的物件引用了 Enumerable 模組,並且實作了 #each 方法可以從 API 回應中取出一些物件。

1class MovieList
2  include Enumerable
3  # ...
4  def each(&block)
5    @api.list.map { |item| Movie.new(item) }.each(&block)
6  end
7end

當我們測試時,除了可以將 @api 以依賴注入的方式替換掉來製作替身,但這樣如果是在一個依賴 MovieList 的物件測試時就很難處理,我們可以對 #each 方法製作替身,來讓我們不用定義複雜的依賴。

 1RSpec.describe MovieListSerializer do
 2  subject(:serializer) { MovieListSerializer.new(list) }
 3
 4  let(:list) { MovieList.new } # API may initialized inside MovieList
 5
 6  before do
 7    allow(list).to receive(:each).and_yield(Movie.new)
 8  end
 9
10  describe '#to_a' do
11    subject { serializer.to_a }
12
13    it { is_expected.to include(be_a(Movie)) }
14  end
15end

如此一來,我們就可以改變 #each 的行為,或者任何需要我們需要 Block 呼叫的情況。

不同回傳

延續前面的例子,假設我們需要測試「多個回傳」該怎麼辦呢?如果只使用一次 #and_yield 的話是無法回傳不同物件的,也因此我們還可以多次呼叫 #and_yield 來傳回多個物件。

 1RSpec.describe MovieListSerializer do
 2  subject(:serializer) { MovieListSerializer.new(list) }
 3
 4  let(:list) { MovieList.new } # API may initialized inside MovieList
 5
 6  before do
 7    allow(list).to receive(:each).and_yield(Movie.new(id: 1))
 8                                 .and_yield(Movie.new(id: 2))
 9  end
10
11  describe '#to_a' do
12    subject { serializer.to_a }
13
14    it { is_expected.to include(have_attributes(id: 1)) }
15    it { is_expected.to include(have_attributes(id: 2)) }
16  end
17end

如果是普通的方法回傳,我們也可以透過告訴 #and_return 來提供複數的回傳狀況。

 1RSpec.describe MovieListSerializer do
 2  subject(:serializer) { MovieListSerializer.new(list) }
 3
 4  let(:list) { MovieList.new } # API may initialized inside MovieList
 5
 6  before do
 7    allow(list).to receive(:empty?).and_return(false, true, true)
 8  end
 9
10  describe '#next_json' do
11    subject { serializer.next_json }
12
13    it { is_expected.not_to be_nil? }
14
15    context 'when movie list is empty' do
16      before { 2.times { serializer.next_json } }
17
18      it { is_expected.to be_nil? }
19    end
20  end
21end

像這樣給 #and_return 多個結果,那麼 RSpec 就會在每次呼叫時回傳下一個,直到沒有可以再繼續回傳為止。

利用這樣的方法,我們就可以刻意的控制像是計數器之類的方法回傳我們期待的數值,來針對特定情況應用。


如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用優雅的 RSpec 測試回饋表單告訴我。