---
title: "優雅的 RSpec 測試 - Mock 與 Stub"
date: 2023-04-07T00:00:00+08:00
publishDate: 2023-04-07T00:00:00Z
lastmod: 2023-09-03T14:30:07+08:00
tags: ["RSpec","教學","測試","Mock","Stub"]
series: "elegant-rspec"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/04/07/elegant-rspec-mock-and-stub/"
language: "zh-tw"
---


在測試中，我們經常會看到 Mock 和 Stub 這兩個詞，很多時候也不容易區分清楚。在 RSpec 中我們基本上不會看到 Mock 或者 Stub 這兩個詞，取而代之的是 Double 和 Allow。

<!--more-->

## Mock

當我們希望某個物件會做出我們「預期」的反應，那麼就可以使用 Mock 的方式來進行處理，通常會是以依賴注入（Dependency Injection）的方式被使用。

舉例來說，假設我們有一個物件會統計合法使用者的數量，但我們只希望驗證「統計」這件事情，就可以對使用者查詢的來源進行 Mock 的處理。

```ruby
class TeamCreator
  def initialize(user_repo)
    @user_repo = user_repo
  end

  def valid_user_count
    @user_repo
      .in_team
      .select(&:valid?)
      .size
  end

  # ...
end
```

針對這個測試，我們可以製作 User Repository 的替身來回傳我們預期的「正確」使用者數量。

```ruby
RSpec.describe TeamCreator do
  subject(:creator) { TeamCreator.new(user_repo)}

  let(:user_repo) { ... }
  # ...

  describe '#valid_user_count' do
    subject { creator.valid_user_count(team: team) }

    let(:team) { create(:team) }
    let(:user_repo) do
      instance_double(
        UserRepository,
        where: build_list(:user, 3, :valid)
      )
    end

    it { is_expected.to eq(3) }
  end
end
```

像這樣，我們可以讓 UserRepository 的 `#where` 方法直接回傳我們預期的內容，這樣就可以在不在資料庫實際建立資料的狀況下通過測試。

## Stub

跟 Mock 不同的是，Stub 只會對「一部分物件」產生改變，也因此我們經常被用來去設定要回傳的測試資料。

我們可以調整上面的測試，改為下面這樣的形式。

```ruby
RSpec.describe TeamCreator do
  subject(:creator) { TeamCreator.new(user_repo)}

  let(:user_repo) { ... }
  # ...

  describe '#valid_user_count' do
    subject { creator.valid_user_count(team: team) }

    let(:team) { create(:team) }
    let(:user_repo) { instance_double(UserRepository) }

    before do
      allow(user_repo).to receive(:where).with(team: team).and_return(build_list(:user, 3, :valid))
    end

    it { is_expected.to eq(3) }
  end
end
```

看起來只是將 `#where` 的定義從 `instance_double` 移出來，改為使用 `#allow` 來定義，這樣的差異在哪裡？

基本上是沒有差異的，更多的時候我們應該採用 Stub 的方式來處理這類情況會更好，因為我們可以指定該收到怎樣的參數。

這其實是 Ruby 的語言特性所造成的，因為我們很容易可以「抽換」物件的實作，也因此大多數時候不太需要製作 Mock 的物件，而是直接對物件本身抽換就能夠處理大多數的情況。

然而 `instance_double` 還是非常有用的，我們會在後續詳細的討論這些應用情境。

---

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

