---
title: "優雅的 RSpec 測試 - Allow 的使用方式"
date: 2023-04-14T00:00:00+08:00
publishDate: 2023-04-14T00: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/14/elegant-rspec-the-allow-usage/"
language: "zh-tw"
---


在 RSpec 的測試中，我們最常會使用到的是 Allow 來改變一個物件的回傳，基本上在需要使用測試替身（Test Double）的地方，就會使用用到。

<!--more-->

## 使用方式{#usage}

要使用 Allow 的方法很簡單，我們只需要定義想要改變的物件以及當哪個方法被呼叫時會回傳什麼即可。

```ruby
RSpec.describe Order do
  let(:product) { build(:product) }

  before do
    allow(product).to receive(:price).and_return(100)
  end

  # ...
end
```

假設我們希望根據不同的參數來回傳不同的內容，則可以改為使用 Block 的方式定義。

```ruby
RSpec.describe Product do
  subject(:product) { build(:product) }

  before do
    allow(product).to receive(:variants) do |type|
      if type == :color
        [build(:product, :with_red_color)]
      else
        []
      end
    end
  end

  # ...

  describe '#variants' do
    subject { product.variants(type) }

    let(:type) { nil }

    it { is_expected.to be_empty }

    context 'when type is color' do
      let(:type) { :color }

      it { is_expected.not_to be_empty }
    end
  end
end
```

像這樣我們就可以根據參數控制回傳或者進行運算，然而這些方式都可能影響測試的正確性，在使用上來說應該更加小心。

除此之外，我們可以用 `#with` 來篩選不同參數該回傳的數值。

```ruby
RSpec.describe Product do
  subject(:product) { build(:product) }

  before do
    allow(:product).to receive(:variants).and_return([])
    allow(:product).to receive(:variants).with(:color).and_return([build(:product, :with_red_color)])
  end

  # ...
end
```

像這樣我們就能夠根據不同的情況指定預期會回傳的數值。

假設我們希望「模糊」的去比對傳入的參數，那麼也可以利用「匹配器」來實現。

```ruby
RSpec.describe AccountHolder do
  let(:account) { build(:bank_account) }

  before do
    # 前面示範的 have_amount 匹配器
    allow(account).to receive(:deposit).with(have_amount(100))
  end

  # ...
end
```

透過匹配器，我們就可以善用像是 `include` 之類的匹配來判斷檔名、物件類型等等不同的參數，就可以簡單地讓特定行為固定回傳某些結果。

假設我們希望拋出錯誤，則可以使用 `#and_raise` 來取代 `#and_return` 進行處理。

> 如果沒有給 `#and_return` 的情況預設回傳 `nil`

```ruby
RSpec.describe UserCreator do
  let(:repo) { UserRepository.new }

  before do
    allow(repo).to receive(:find!).and_raise(UserNotFound)
  end

  # ...
end
```

善用這些替代方法的機制，我們基本上就能夠處理大多數測試的情況。

## Allow Any Instance Of

跟 Allow 一樣，經常被使用的還有 `allow_any_instance_of` 這個用法，它可以讓我們「一次性」的對某個類別的實例（Instance）進行 Stub 的處理，然而 Rubocop 並不推薦。

這是因為當我們需要像這樣處理時，就表示我們在 Dependency Injection（依賴注入）的地方沒有很好的處理造成耦合，因此需要透過這種方式來解決。

舉例來說，我們有一個像這樣的物件。

```ruby
class UserCreator
  def initialize
    @repo = UserRepository.new
  end

  def verify_email(user_id)
    user = @repo.find(user_id)
    RegistrationMailer.verify(user).deliver_now
  end

  # ...
end
```

當我們想要讓 `UserRepository#find` 直接回傳特定使用者來驗證 `UserCreator#verify_email` 有正確的動作時，就會因為無法製作替身而需要使用 `allow_any_instance_of` 來處理。

如果改為使用依賴注入的方式，只需要調整為

```ruby
class UserCreator
  def initialize(repo)
    @repo = repo
  end

  # ...
end
```

這樣在撰寫測試時，就可以將「替身」注入到裡面，一方面讓物件的可測試性提升，另外一方面也幫助我們降低了耦合度，關於這部分的討論會在後面的文章詳細說明。

---

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

