---
title: "優雅的 RSpec 測試 - 共用案例"
date: 2023-03-10T00:00:00+08:00
publishDate: 2023-03-10T00:00:00Z
lastmod: 2023-09-03T14:30:07+08:00
tags: ["RSpec","教學","測試","案例","Example"]
series: "elegant-rspec"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/03/10/elegant-rspec-shared-example/"
language: "zh-tw"
---


在我們在撰寫測試的過程中，可能因為同一類型的物件行為有著類似的情況，因此會需要重複的專寫相似的測試案例，這個時候我們就可以利用「共用案例（Shared Example）」的機制來重複利用這些測試案例。

<!--more-->


## 使用方式{#usage}

要定義一個共用案例，可以使用 `.shared_examples` 來進行，因為是 Class Method（類別方法）因此可以直接在測試中撰寫，或者另外新增一個檔案來進行定義。

```ruby
RSpec.describe 'User API', type: :api do
  shared_examples 'successful response' do
    it { is_expected.to be_successful }
  end

  describe 'GET /users' do
    # ...
    it_behaves_like 'successful response'
  end

  describe 'GET /users/:id' do
    # ...
    it_behaves_like 'successful response'
  end
end
```

像這樣子我們就可以在所有預期會有「成功回傳」的測試中重複使用這些共用的測試案例，要注意的是使用共用案例有以下限制。

## 範圍{#scope}

根據我們定義共用案例時機，會讓可以引用的範圍受到不同程度的限制。基本原則是「下層可以繼承上層」的方式。

舉例來說，如果是跨 Context（情境）的狀況，是無法被共用的。

```ruby
RSpec.describe 'User API', type: :api do
  describe 'GET /users' do
    shared_examples 'user list' do
      it { is_expected.to include('users' => be_a(Array)) }
    end
    # ...

  end

  describe 'GET /admin/users' do
    # ...
    it_behaves_like 'user list'
  end
end
```

因為 `GET /users` 和 `GET /admin/users` 是相同層級，因此會無法飲用到另一個層級的共用案例，要解決這個問題只需要向上放置即可。

也因此，如果是經常被共用的案例，我們可以直接對整個 RSpec 進行定義。

```ruby
RSpec.shared_examples 'user list' do
  it { is_expected.to include('users' => be_a(Array)) }
end
```

如此一來，我們只需要在運行測試前載入這個共用案例的定義就可以使用，而不需要在不同的測試案例中去定義。

## 參數{#parameters}

我們在前面的例子中定義了 `user list` 這個共用案例，但大多數 API 回應都是列表，我們可以利用參數的機制讓他變成 `list resources` 之類的共用案例。

```ruby
RSpec.shared_examples 'list resources' do |resource:|
  it { is_expected.to include(resource => be_a(Array)) }
end
```

在使用時，我們就可以額外的提供參數來使用。

```ruby
RSpec.describe 'User API', type: :api do
  describe 'GET /users' do
    # ...
    it_behaves_like 'list resources', resource: 'users'
  end

  describe 'GET /admin/users' do
    # ...
    it_behaves_like 'list resources', resource: 'admin_users'
  end
end
```

像這樣，我們就可以提供不同類型的 List API 來使用這個共用案例，而不需要重複定義多次。

除此之外，我們也可以傳入一個 Block 在共用案例被執行之前呼叫，來做一些對應的處理。

```ruby
RSpec.describe 'User API', type: :api do
  describe 'GET /users' do
    # ...
    it_behaves_like 'list resources', resource: 'users' do
      before { API.adapter = MockUserAPI }
    end
  end

  describe 'GET /admin/users' do
    # ...
    it_behaves_like 'list resources', resource: 'admin_users' do
	  before { API.adapter = MockAdminUserAPI }
    end
  end
end
```

如此一來我們在共用案例上就能有非常多的彈性，來對應那類有著「類似行為」的測試。下一篇我們會介紹「自訂匹配器」並且分析跟共用案例的差異。

---

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

