---
title: "優雅的 RSpec 測試 - Spy 的應用"
date: 2023-04-28T00:00:00+08:00
publishDate: 2023-04-28T00: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/28/elegant-rspec-the-spy-usage/"
language: "zh-tw"
---


在撰寫 RSpec 的過程中，我們大多會使用 `expect`（預期）搭配 `receive` （接收）來驗證某個方法有被呼叫，然而這會讓我們需要將「預期」寫在實際的行為之前，在驗證的邏輯上似乎有點奇怪，因此我們可以用 Spy 功能替代，在呼叫實際的方法後再去驗證行為。

<!--more-->

## 使用方式{#usage}

Spy 的使用可以預期跟 Double（替身）的方法類似，兩者都是由 RSpec 生成一個暫時性的物件，不同的地方在於我們使用 `instance_double` 建立物件的實例後是無法呼叫沒有被定義的方法，然而 `instance_spy` 則預設會接受這些方法的呼叫。

舉例來說，我們希望在特定情況發生時會輸出紀錄檔，用於後續的追蹤。

```ruby
RSpec.describe PlanUpgradeService do
  subject(:service) { UpgradeService.new(logger) }

  let(:logger) { instance_spy(Logger) }

  describe '#check_plan' do
    subject(:check) { service.check_plan }

    # ...
    context 'when plan not available' do
      it 'is expected log not available plan' do
        check
        expect(logger).to have_received(:warn)
      end
    end
  end
end
```

像這樣子，我們就可以檢驗當我們在進行方案升級的過程中遇到了不存在的方案，會寫入一筆紀錄方便後續追蹤原因，因為是在方法呼叫過程中呼叫的行為無法直接驗證，但可以透過注入 Spy 物件來檢驗這件事情。

## 驗證參數{#verify-parameters}

雖然我們可以驗證方法被呼叫，但實作中可能有不同情況會呼叫這個方法，這個時候就可以利用 `#with` 跟匹配器搭配，來協助我們處理這件事情。

```ruby
RSpec.describe PlanUpgradeService do
  # ...
  describe '#check_plan' do
    subject(:check) { service.check_plan }

    # ...
    context 'when plan not available' do
      it 'is expected log not available plan' do
        check
        expect(logger).to have_received(:warn).with(/Unknown plan: (.*)+/)
      end
    end
  end
end
```

像這樣子加入 `with(/.../)` 就可以利用正規表達式的方法來檢測是否在呼叫 `Logger#warn` 時是以 `warn("Unknown plan: #{plan_id}"` 的格式輸出，這樣也可以確保我們在 Graylog 或者 Loki 這類日誌服務中設定的篩選規則可以正確運作，不會因為修改了日誌輸出就造成無法追蹤到特定問題。

##  反思{#review}

關於 RSpec 大部分的常用功能到此就差不多介紹完畢，在開始下一篇文章之前我們可以先暫停下來思考一些問題。

我在 2022 年曾經寫過 [2022 年的 RSpec 測試還需要 Mock 嗎？](https://blog.aotoki.me/posts/2022/08/12/do-we-need-mock-in-rspec-in-2022/) 這篇文章，其實不難發現在非常多的情境下，我們是幾乎不需要去製作測試替身的。假設一個測試中使用了非常多的測試替身真的是一件好事嗎？

當我們遇到需要使用測試替身的情境時，是真的完全無法避免嗎？有沒有可能可以在大多數的情境下，都不借助測試替身就完成測試，我認為是一個需要深思的問題。我們使用 Ruby 可以非常輕鬆的製作各種替身，同時也可能讓我們忘記去思考是因為「設計問題」造成必須製作測試替身，還是真的必須使用測試替身。

---

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

