我們了解了常見的匹配方式之後,接下來我們要進一步善用這些匹配器來做到更加優雅的比對,讓我們的測試看起來更容易閱讀。
Has Attributes
在 RSpec 的測試中我們會設定「主旨(Subject)」來定義測試的對象,假設想要確認某個屬性是否符合預期,就可以用 has_attributes
匹配器來處理。
一般來說,大多數人會用這樣的方式進行驗證。
1RSpec.describe User do
2 subject(:user) { User.new }
3
4 # Assertation Style
5 it { expect(user.permissions).to eq(%i[read write]) }
6
7 # Describe Style
8 describe '#permissions' do
9 subject { user.permissions }
10
11 it { is_expected.to eq(%i[read write]) }
12 end
13end
然而這兩個方式都有其缺點,使用斷言的時候因為我們用了 user.permissions
作為預測對象,這就破壞了主旨。使用 describe
切分出測試群組後,雖然主旨沒有問題,但 describe
比較適合做用於「動作」而非屬性。
也因此,最為適合的方式應該是透過 has_attributes
進行匹配
1RSpec.describe User do
2 subject(:user) { User.new }
3
4 it { is_expected.to has_attributes(permissions: %i[read write]) }
5end
這樣就可以很清晰的表示 User 物件具備了 #permissions
這一個屬性可以被讀取。
Has Attributes 匹配器是允許同時匹配多個屬性,然而這樣在發生錯誤時時我們就需要同時檢查多個屬性,因此我會偏好一次只檢查一個屬性。
Include
Include 匹配器可以檢查陣列這類物件是否包含某個元素,跟 Be 匹配器類似的地方在於,他需要該物件具備 #include?
方法才可以生效,因此並不一定限於陣列物件。
1RSpec.describe Reader do
2 subject(:reader) { Reader.new }
3
4 describe '#items' do
5 subject { reader.items }
6 before { reader.read! }
7
8 it { is_expected.to include('First Line') }
9 end
10end
在 Has Attributes 的例子中,我們會檢查 #permissions
屬性回傳了一個陣列,並且同時包含了 read
和 write
兩個符號(Symbol)來表示權限,然而這個設計會讓我們在針對不同權限的檢查時需要撰寫非常冗長的驗證,我們也可以利用 Include 匹配器來處理。
1RSpec.describe User do
2 subject { User.new }
3
4 it { is_expected.to have_attributes(permissions: include(:read)) }
5
6 context 'when role is admin' do
7 it { is_expected.to have_attributes(permissions: include(:write)) }
8 end
9end
這是 RSpec 匹配器非常強大的一個部分,我們被允許巢狀(Nested)的使用大部分匹配器,如此一來就能在不破壞「可讀性」的狀況下做出更加精細的斷言。
1RSpec.describe Order do
2 subject(:order) { Order.new }
3
4 it { is_expected.to have_attributes(items: include(be_discountable)) }
5end
以上面例子來看,我們可以檢查商品中包含可折扣的品項,因此巢狀匹配也不限於單一層級,不過使用上還是要根據狀況判斷,如果是非常複雜的結構也該考慮設計是否還能在進行改進。
Match
還有一些情況我們會希望知道「部分文字訊息」而不是完整的內容,可能像是發送給會員的通知信,裡面正確的套用了「會員暱稱」這類情況,我們就可以用 Match 匹配器來進行處理。
1RSpec.describe UserSMSNotification do
2 subject(:notification) { UserSMSNotification.new }
3
4 describe '#shipping_started' do
5 subject { notification.shipping_started(user: user) }
6
7 let(:user) { User.new(name: 'Aotokitsuruya') }
8
9 it { is_expected.to have_attributes(body: include('Hello Aotokitsuruya')) }
10 it { is_expected.to have_attributes(body: match(/Shipping Fee: $\d+/)) }
11 end
12end
因為字串同時實作了 #include
和 #match
兩個方法,因此我們可以針對「固定」的字串使用 #include
直接檢查包含某段文字,並且用 #match
來比對某段文字以及動態的內容。
如此一來我們就能非常彈性的去檢查一個物件所包含的狀態,以及經過一系列動作後所呈現的狀態。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用優雅的 RSpec 測試回饋表單告訴我。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 優雅的 RSpec 測試 - 前言
- 優雅的 RSpec 測試 - 撰寫測試的方式
- 優雅的 RSpec 測試 - RSpec 概觀
- 優雅的 RSpec 測試 - 測試案例
- 優雅的 RSpec 測試 - 組織測試
- 優雅的 RSpec 測試 - 前置處理
- 優雅的 RSpec 測試 - 常見匹配器
- 優雅的 RSpec 測試 - 內容匹配
- 優雅的 RSpec 測試 - 錯誤匹配
- 優雅的 RSpec 測試 - 共用案例
- 優雅的 RSpec 測試 - 自訂匹配器
- 優雅的 RSpec 測試 - 輔助方法
- 優雅的 RSpec 測試 - 測試替身
- 優雅的 RSpec 測試 - Mock 與 Stub
- 優雅的 RSpec 測試 - Allow 的使用方式
- 優雅的 RSpec 測試 - Allow 變化應用
- 優雅的 RSpec 測試 - Spy 的應用
- 優雅的 RSpec 測試 - 物件的可測試性
- 優雅的 RSpec 測試 - 耦合與依賴注入
- 優雅的 RSpec 測試 - 探索式的測試與重構