---
title: "優雅的 RSpec 測試 - Allow 變化應用"
date: 2023-04-21T00:00:00+08:00
publishDate: 2023-04-21T00:00:00Z
lastmod: 2023-09-03T14:30:07+08:00
tags: ["RSpec","教學","測試","Stub"]
series: "elegant-rspec"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/04/21/elegant-rspec-another-allow-usage/"
language: "zh-tw"
---


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

<!--more-->

## Each 的替身{#the-test-double-of-each}

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

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


```ruby
class MovieList
  include Enumerable
  # ...
  def each(&block)
    @api.list.map { |item| Movie.new(item) }.each(&block)
  end
end
```

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

```ruby
RSpec.describe MovieListSerializer do
  subject(:serializer) { MovieListSerializer.new(list) }

  let(:list) { MovieList.new } # API may initialized inside MovieList

  before do
    allow(list).to receive(:each).and_yield(Movie.new)
  end

  describe '#to_a' do
    subject { serializer.to_a }

    it { is_expected.to include(be_a(Movie)) }
  end
end
```

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

## 不同回傳{#different-return}

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

```ruby
RSpec.describe MovieListSerializer do
  subject(:serializer) { MovieListSerializer.new(list) }

  let(:list) { MovieList.new } # API may initialized inside MovieList

  before do
    allow(list).to receive(:each).and_yield(Movie.new(id: 1))
                                 .and_yield(Movie.new(id: 2))
  end

  describe '#to_a' do
    subject { serializer.to_a }

    it { is_expected.to include(have_attributes(id: 1)) }
    it { is_expected.to include(have_attributes(id: 2)) }
  end
end
```

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


```ruby
RSpec.describe MovieListSerializer do
  subject(:serializer) { MovieListSerializer.new(list) }

  let(:list) { MovieList.new } # API may initialized inside MovieList

  before do
    allow(list).to receive(:empty?).and_return(false, true, true)
  end

  describe '#next_json' do
    subject { serializer.next_json }

    it { is_expected.not_to be_nil? }

    context 'when movie list is empty' do
      before { 2.times { serializer.next_json } }

      it { is_expected.to be_nil? }
    end
  end
end
```

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

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

---

如果想在第一時間收到更新，歡迎[訂閱弦而時習之](https://mailchi.mp/aotoki/graceful-rspec)在這系列文章更新時收到通知，如果有希望了解的知識，可以利用[優雅的 RSpec 測試回饋表單](https://us4.list-manage.com/survey?u=dd3d68032c0510041f1302539&id=5ddf86cae1&attribution=false)告訴我。

