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

優雅的 RSpec 測試 - 組織測試

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

Context

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

 1RSpec.describe User do
 2  subject { User.new }
 3
 4  it { is_expected.not_to be_admin }
 5
 6  context 'when role is admin' do
 7    subject { User.new(role: :admin) }
 8
 9    it { is_expected.to be_admin }
10  end
11end

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

Describe

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

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

 1RSpec.describe User do
 2  subject(:user) { User.new }
 3
 4  # ...
 5  describe '#confirm!' do
 6    subject(:force_confirm) { user.confirm! }
 7
 8    it { expect { force_confirm }.to raise_error(InvalidEmailError) }
 9
10    context 'when email is valid' do
11      let(:user) { User.new(email: '[email protected]') }
12
13      it { is_expected.to be_nil }
14    end
15  end
16end

由上面的案例可以看到,基本上使用方式跟 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 物件,這讓我們的測試寫起來會有不少重複的部分,因此可以改寫成這樣。

 1RSpec.describe User do
 2  subject { User.new(role: role) }
 3
 4  let(:role) { nil }
 5
 6  it { is_expected.not_to be_admin }
 7
 8  context 'when role is admin' do
 9    let(:role) { :admin }
10
11    it { is_expected.to be_admin }
12  end
13end

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

何時該使用 Let 可以參考 COSCUP 2022 - RSpec or Let’s Not 這場議程的分析。

限制

RSpec 提供了我們非常方便的 DSL 來撰寫測試,然而過度濫用還是會讓測試難以閱讀,因此在 Rubocoprubocop-rspec 風格指南中,還是有以下限制。

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

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

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


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