在這系列的文章中,我們已經對使用 RSpec 進行測試的技巧有一定的理解,接下來我們來討論一下「可測試性」這件事情。
難以測試
在理想的狀況下,我們會認為所有的程式都能可以被測試。但為什麼會出現「難以測試」這樣的情況呢?其實原因有非常多,像是過度的「耦合」造成我們無法控制物件依賴,進而無法使用測試替身之類的方式來輔助測試。
簡單來說,假設我們的物件設計不好,可能會造成在撰寫測試上的困難。不過「容易測試」的物件,並不一定是一個好用、合理的物件,一個良好的物件設計需要考量的地方是非常多的。
理想的物件
假設有一個物件是非常容易被測試的,大多是我們可以很容易的觀察到內部狀態(Instance Variable,實例變數)的狀況,並且每個行為都能獨立的動作而不需要依賴其他前置動作。
舉例來說,像是下面這樣的物件非常好測試,基本上就類似於 Rails 框架的 Model,也因此大多數專案通常都會優先加入 Model 類型的測試。
1class Worker
2 attr_accessor :stopped
3
4 def initialize(stopped: true)
5 @stopped = stopped
6 end
7
8 def start
9 @stopped = false
10 end
11
12 def stop
13 @stopped = true
14 end
15
16 def stop?
17 @stopped == true
18 end
19end
對應的測試可以寫成這樣,非常簡單且直覺。
1RSpec.describe Worker do
2 subject(:worker) { Worker.new }
3
4 it { is_expected.to be_stop }
5
6 describe '#start' do
7 before { worker.start }
8
9 it { is_expected.not_to be_stop }
10 end
11
12 describe '#stop' do
13 subject(:worker) { Worker.new(stoppted: false) }
14
15 before { worker.stop }
16
17 it { is_expected.to be_stop }
18 it { expect { worker.stop }.to change(worker, :stopped).from(false).to(true) }
19 end
20end
私有方法
雖然在 Ruby 中我們可以利用 #send
方法呼叫私有方法(Private Method),然而在測試中我們應該專注於公有方法(Public Method)的呼叫,要覆蓋到這些私有方法必須透過公有方法來涵蓋到,這也改善了我們程式中存在 Dead Code(沒有任何人使用)的比例。
假設我們有一個 Subscription 物件管理了可用的服務,只有在「已訂閱」的狀態下才能啟用服務,因此我們可能會提供 #active
方法替我們處理這件事情,同時加入一個私有的 #append_service
方法來協助我們增加可用的服務。
1class Subscription
2 attr_reader :services
3
4 def active(service_name)
5 return false if activated?
6
7 append_service(name: service_name)
8 end
9
10 private
11
12 def append_service(name:)
13 return @services if @services.include?(name)
14
15 @services += [name]
16 end
17end
在撰寫對應的測試時,我們並不需要去測試 #append_service
是否正確,而是要驗證 #active
方法是否可以正常使用,以及結果是否符合我們預期的最終結果。
1RSpec.describe Subscription do
2 # ...
3 describe '#active' do
4 subject(:active) { subscription.active('unlimited_reader') }
5
6 it { is_expected.to be_falsy }
7
8 context 'when subscription is actived' do
9 # ...
10
11 it { is_expected.to include('unlimited_reader') }
12 end
13
14 context 'when subscription is actived and activated unlimited_reader' do
15 before { subscription.active('unlimited_reader') }
16
17 it { is_expected.to include('unlimited_reader') }
18 end
19 end
20end
像這樣子,我們透過 Subscription 的單元測試涵蓋了 #active
方法相關的情境(訂閱未啟用、啟用後第一次加入服務、服務已存在)來完整和蓋所有私有方法的行為。
要特別注意的地方是,這樣雖然能提高「覆蓋率」然而很可能是「為測試而測試」而不是真正的去測試所有真正被使用到的情境,在這種情境下透過觀察 E2E Testing 的涵蓋率以及比對功能定義,會比使用單元測試的覆蓋率更準確。
想要更近一步的解決複雜的問題,接下來會討論「依賴注入」來減少耦合,同時也能讓我們更容易測試到一些難以測試的依賴。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用優雅的 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 測試 - 探索式的測試與重構