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

優雅的 RSpec 測試 - Mock 與 Stub

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

Mock

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

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

 1class TeamCreator
 2  def initialize(user_repo)
 3    @user_repo = user_repo
 4  end
 5
 6  def valid_user_count
 7    @user_repo
 8      .in_team
 9      .select(&:valid?)
10      .size
11  end
12
13  # ...
14end

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

 1RSpec.describe TeamCreator do
 2  subject(:creator) { TeamCreator.new(user_repo)}
 3
 4  let(:user_repo) { ... }
 5  # ...
 6
 7  describe '#valid_user_count' do
 8    subject { creator.valid_user_count(team: team) }
 9
10    let(:team) { create(:team) }
11    let(:user_repo) do
12      instance_double(
13        UserRepository,
14        where: build_list(:user, 3, :valid)
15      )
16    end
17
18    it { is_expected.to eq(3) }
19  end
20end

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

Stub

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

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

 1RSpec.describe TeamCreator do
 2  subject(:creator) { TeamCreator.new(user_repo)}
 3
 4  let(:user_repo) { ... }
 5  # ...
 6
 7  describe '#valid_user_count' do
 8    subject { creator.valid_user_count(team: team) }
 9
10    let(:team) { create(:team) }
11    let(:user_repo) { instance_double(UserRepository) }
12
13    before do
14      allow(user_repo).to receive(:where).with(team: team).and_return(build_list(:user, 3, :valid))
15    end
16
17    it { is_expected.to eq(3) }
18  end
19end

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

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

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

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


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