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

優雅的 RSpec 測試 - 測試案例

在我的經驗中,要將測試寫好並不是一件容易的事情。很多時候,我們會看到不少「測試」是難以閱讀的,這也讓我們很難了解到測試的意圖,因此我們要先針對「結構」進行一些討論。

Subject

Subject(主旨)是 RSpec 測試中的一個重要概念,非常多人在撰寫 RSpec 測試時沒有設定主旨,也因此會讓我們難以理解測試的「對象」以及需要製作大量的假設物件才能夠完整進行測試。

在預設的狀況下,測試的主旨會是物件的實例(Instance)會隱含的呼叫 #new 方法建立。

1class User
2  attr_reader :name
3end
4
5RSpec.describe User do
6  it { is_expected.to have_attributes(name: nil) }
7end

在大多數的狀況下,我們會需要去產生對應的物件,因此可以明確的使用 subject 關鍵字定義測試的對相。

1RSpec.describe User do
2  subject { User.new(name: 'Aotoki') }
3
4  it { is_expected.to have_attributes(name: 'Aotoki') }
5end

It

It(案例)之所以會用 it 這個代名詞,是為了跟 subject 搭配使用,我們在撰寫測試的時候應該要以像是 “it is expected to be truthy” 的「句子」去理解,也因此這裡的 it 就是指 subject 所定義的對象。

在撰寫測試案例的時候,我們應該像這樣去描述一個情境(Scenario)

1RSpec.describe User do
2  subject { User.new(name: 'Aotoki', role: :admin) }
3
4  it { is_expected.to be_admin }
5  it { is_expected.to have_attributes(name: 'Aotoki') }
6end

如此一來,我們會很自然的得到「他預期作為管理員」和「他預期有名字叫做蒼時」這樣的句子呈現,然而大多數人會寫成像是這樣的測試案例。

 1RSpec.describe User do
 2  # 情況一
 3  let(:user) { User.new(name: 'Aotoki', role: :admin) }
 4  it 'have a name' { expect(user.name).to eq('Aotoki') }
 5
 6  # 情況二
 7  it 'is a admin' do
 8    user = User.new(name: 'Aotoki', role: :admin)
 9    expect(user.role).to eq(:admin)
10  end
11end

除了測試案例會佔用非常大面積之外,也有非常多重複、難以閱讀的地方,我們應該以「單行」呈現作為目標。

使用 be_admin 是 RSpec 內建的 Matcher(配對)機制,我們會在後面的章節詳細討論這個使用方式所帶來的好處。

Expect

Expect(預期)基本上跟其他語言的 Assertation(斷言)是差不多的概念,我們都會用來描述一個測試案例「預期」出現的結果,也因此相比其他語言使用 assert(expected, actual) 的育法結構,RSpec 善用了 DSL 的特性製作出了更加語意化的測試案例。

在大多數情境,我會推薦使用 is_expected 的斷言,而非 expect 來描述,如果是對屬性、狀態類型的確認,用 is_expected 會更加的清晰。

1RSpec.describe User do
2  subject { User.new(name: 'Aotoki') }
3
4  it { is_expected.to have_attributes(name: 'Aotoki') }
5end

以這個例子來說,我們可以用 “it is expected to have attributes name is Aotoki” 這樣的方式理解,非常的簡潔清晰,而且只有一行也非常容易閱讀,而不需要花費時間理解。

如果是針對行為類型的測試,那麼我們就可以使用 expect 來進行處理,像是下面這樣的案例。

1RSpec.describe User do
2  # ...
3  subject(:confirm!) { user.confirm! }
4
5  it { expect { confirm! }.to raise_error(User::InvalidEmailAddress) }
6  it { expect { confirm! }.to change(User.valid, :count).by(1) }
7end

此時我們的語法會變為動詞的描述,因此會像是 “it expect confirm to raise error user has invalid email address” 的句型,或者 “it expect confirm to change valid user count by 1” 這樣的句子,雖然有一小部分需要自己「補充」然而大多數都還在可以理解的範圍內。

如果能好好的實踐這個原則,我們大多數的測試都可以保持在一行的案例,並且能夠非常容易的被其他人所閱讀。

這個原則會讓我們在實作的時候,對於「物件封裝」有更加深入的思考,像是當我們必須用 expect(JSON.parse(res.body)) 這樣的語法起頭的時候,就可能是我們的物件沒有正確封裝造成我們必須在測試做額外的處理才能夠驗證,這樣就表示我們並非驗證「屬性」也不是「行為」而是透過「新的邏輯」去測試某段實作,很可能就有設計不良的問題存在。


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