---
title: "優雅的 RSpec 測試 - 組織測試"
date: 2023-02-03T00:00:00+08:00
publishDate: 2023-02-03T00:00:00Z
lastmod: 2023-09-03T14:30:07+08:00
tags: ["RSpec","教學","測試","結構","組織"]
series: "elegant-rspec"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/02/03/elegant-rspec-organize-test/"
language: "zh-tw"
---


了解如何撰寫基本的測試後，我們需要學習如何組織一個測試。在進行單元測試的時候，我們不太可能單純針對一個方法測試，而是會針對整個物件進行驗證，因此需要區分不同情境、測試目標。

<!--more-->

## Context

Context（上下文）簡單來說就是情境的意思，在語意上的表達是用來對應「不同情況」的呈現，假設我們想要測試「角色為管理員」以及「角色為使用者」兩種不同的情況，就可以利用 `context` 來進行切分。

```ruby
RSpec.describe User do
  subject { User.new }

  it { is_expected.not_to be_admin }

  context 'when role is admin' do
    subject { User.new(role: :admin) }

    it { is_expected.to be_admin }
  end
end
```

像這樣我們就可以很清楚的透過 `when role is admin` 描述當角色為管理員的情況下，特定行為會有怎樣的動作。

## Describe

Describe（描述）基本上跟 Context 是沒有差異的，兩者都能夠產生一個「獨立」的測試群組（Group），讓 Subject（主旨）以及其他測試相關的設定分離開來，然而在使用上我們可以利用不同的語意，來區分出測試程式的「意圖」

假設我們想要針對「驗證使用者」這個行為進行測試，那們我們可以像這樣實作。

```ruby
RSpec.describe User do
  subject(:user) { User.new }

  # ...
  describe '#confirm!' do
    subject(:force_confirm) { user.confirm! }

    it { expect { force_confirm }.to raise_error(InvalidEmailError) }

    context 'when email is valid' do
      let(:user) { User.new(email: 'demo@example.com') }

      it { is_expected.to be_nil }
    end
  end
end
```

由上面的案例可以看到，基本上使用方式跟 Context 是完全相同的，但是透過 `describe - context - it` 的組合，我們可以得到類似這樣的測試報告。

```
User
  #confirm!
    is expect force_confirm to raise error InvalidEmailError
    when email is valid
      is expected to be nil
```

如此一來，我們在 RSpec 的程式碼部分可以很快速的看出來現在正在做什麼，同時也讓報告的輸出變的非常容易閱讀。

簡單來說，當我們使用 Describe 時通常會改變 Subject 來調整測試的對象，使用 Context 的時候會搭配 Let 來控制測試的變化。

## Let

Let 可以用來控制測試的條件變化，因此我們可以在 Context 之間利用 Let 賦予不同的數值，來針對測試的條件進行調整。

要注意的是，並不是所有的情況都需要使用 Let 來改變，如果是不影響測試的數值，那們我們就應該直接寫死而不是撰寫非常多的 Let 去控制。

舉例來說，在一開始的 Context 範例中「角色為管理員」其實都是對 User 物件進行測試，然而我們用 Subject 重新產生了 User 物件，這讓我們的測試寫起來會有不少重複的部分，因此可以改寫成這樣。

```ruby
RSpec.describe User do
  subject { User.new(role: role) }

  let(:role) { nil }

  it { is_expected.not_to be_admin }

  context 'when role is admin' do
    let(:role) { :admin }

    it { is_expected.to be_admin }
  end
end
```

在這裡，測試的語意就會變成「當角色為管理員時，使『角色』為管理員，並且預期是管理員」會比改變測試主旨更加符合當下的語境。

> 何時該使用 Let 可以參考 [COSCUP 2022 - RSpec or Let's Not](https://coscup.org/2022/zh-TW/session/R3GDVM) 這場議程的分析。

## 限制{#limitation}

RSpec 提供了我們非常方便的 DSL 來撰寫測試，然而過度濫用還是會讓測試難以閱讀，因此在 [Rubocop](https://rubocop.org/) 的 `rubocop-rspec` 風格指南中，還是有以下限制。

* 至多兩層的巢狀組成
* 每一個區塊最多五個 Let 定義

簡單來說，我們在使用 `describe` 和 `context` 的時候最多就是兩個層級（方法以及該方法在不同輸入的狀況），大多數的物件如果沒有過度複雜，應該是可以很輕鬆符合這個條件。

至於 Let 至多五個，在某些測試可能很容易超過，但這也表示我們的輸入過度的複雜，畢竟建議的參數（Parameters）數量通常也是不超過五個，這就該檢討我們的物件設計。也因此，能避免的話還是盡可能避免，這也是為什麼會建議「能寫死就以寫死優先」的理由。

---

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

