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

優雅的 RSpec 測試 - 內容匹配

這篇文章是 優雅的 RSpec 測試 系列的一部分。

我們了解了常見的匹配方式之後,接下來我們要進一步善用這些匹配器來做到更加優雅的比對,讓我們的測試看起來更容易閱讀。

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 屬性回傳了一個陣列,並且同時包含了 readwrite 兩個符號(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 測試回饋表單告訴我。