---
title: "優雅的 RSpec 測試 - 錯誤匹配"
date: 2023-03-03T00:00:00+08:00
publishDate: 2023-03-03T00:00:00Z
lastmod: 2023-09-03T14:30:07+08:00
tags: ["RSpec","教學","測試","匹配","Matcher"]
series: "elegant-rspec"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/03/03/elegant-rspec-error-matcher/"
language: "zh-tw"
---


前面兩篇文章已經將比較常會被使用的匹配器介紹完畢，接下來我們要再介紹「錯誤」跟「滿足」兩種匹配器，這些匹配器的使用就可以對應大多數的情況。除此之外，RSpec 還提供非常豐富的[匹配器](https://relishapp.com/rspec/rspec-expectations/v/3-11/docs/built-in-matchers)可以使用，在撰寫斷言之前可以先去查詢適合的匹配器。

<!--more-->

## Raise Error

Raise Error 匹配器的使用因人而異，然而就我個人的角度來看我們應該多善用這個匹配器在有例外機制的語言上，適當的使用可以讓我們的邏輯、實作都更加清晰。

舉例來說，我們有一個「最大商品數量」的限制，可以實作一個 Service Object 來處理這件事情。

```ruby
class AddCartService
  def initialize(cart)
    @cart = cart
  end

  def check_amount_of(product)
    return unless product.max_amount_limit?
    return if product.max_amount >= @card.amount_of(product)

    raise MaxProductExceededError
  end

  # ...
end

# In Controller
add_cart_service.check_amount_of(product)
add_cart_service.some_other_check(product)
add_cart_service.add_item(product)

cart.save
```

那麼，我們只需要驗證「當商品數超過時，必須拋出錯誤」這樣的資訊即可。

```ruby
RSpec.describe AddCartService do
  subject(:service) { AddCartService.new(cart) }

  let(:cart) { create(:cart) }

  describe '#check_amount_of' do
    subject(:check_amount) { service.check_amount_of(product) }

    let(:product) { create(:product, max_amonut: 5) }

    it { is_expected.to be_nil }

    context 'when not reach max amount' do
      before { 4.times { service.add_item(product) } }

      it { is_expected.to be_nil }
    end

    contxt 'when reach max amonut' do
      before { 5.times { service.add_item(product) } }

      it { expect { check_amonut }.to raise_error(MaxProductExceededError) }
    end
  end
end
```

像這樣子，我們就可以利用 Raise Error 匹配器，追蹤在呼叫方法的過程中是否有錯誤被拋出。

> 上面的例子可能不算太恰當，因為我們可以在 Rails 中利用 Validation 實踐這件事情，因此需要根據當下的情境判斷是否應該使用錯誤的方式處理。


## Satisfy

Satisfy 匹配器算是一個相當特殊的匹配器，假設我們要檢查的數值不容易用固定數值判定，或者需要滿足一些特殊的邏輯，那麼使用 Satisfy 匹配器處理這類問題可能是不錯的方式。

假設我們有一個非常詭異的 API 需要測試，又無法控制行為（可能是來自第三方套件）那麼就可以利用 Satisfy 匹配器來對應這類情況。

```ruby
RSpec.describe OddAPI do
  subject(:api) { OddAPI.new }

  describe '#odd_behavior' do
    subject { app.odd_behavior }

    it { is_expected.to satisfy { |res| res.state.split(':').first == 200 } }
  end
end
```

大多數的情況下我們都不應該使用這樣的方式處理，比較好的方式可以透過「自訂匹配器」的方式處理，我們會在後續的文章說明。

我們只有在非常少見的特殊情況使用這個方法來處理，或者需要「明確表達」特定資訊的狀況，像是 `{ |value| value % 2 == 1 }` 來表示計算後永遠為奇數。

---

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

