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

優雅的 RSpec 測試 - Spy 的應用

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

使用方式

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

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

 1RSpec.describe PlanUpgradeService do
 2  subject(:service) { UpgradeService.new(logger) }
 3
 4  let(:logger) { instance_spy(Logger) }
 5
 6  describe '#check_plan' do
 7    subject(:check) { service.check_plan }
 8
 9    # ...
10    context 'when plan not available' do
11      it 'is expected log not available plan' do
12        check
13        expect(logger).to have_received(:warn)
14      end
15    end
16  end
17end

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

驗證參數

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

 1RSpec.describe PlanUpgradeService do
 2  # ...
 3  describe '#check_plan' do
 4    subject(:check) { service.check_plan }
 5
 6    # ...
 7    context 'when plan not available' do
 8      it 'is expected log not available plan' do
 9        check
10        expect(logger).to have_received(:warn).with(/Unknown plan: (.*)+/)
11      end
12    end
13  end
14end

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

反思

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

我在 2022 年曾經寫過 2022 年的 RSpec 測試還需要 Mock 嗎? 這篇文章,其實不難發現在非常多的情境下,我們是幾乎不需要去製作測試替身的。假設一個測試中使用了非常多的測試替身真的是一件好事嗎?

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


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