雖然我們沒有使用依賴注入的技巧,或者實作中有耦合的問題存在,在 Ruby 中因為語言特性的關係,我們還是有非常多方式可以進行測試,然而這也會讓我們的測試變得複雜。
什麼是耦合
耦合的情況有蠻多種類型的,不論是哪一種類型都會讓測試不容易處理。舉例來說,有一個物件需要另一個物件協助才能處理某件事情,這個時候就會產生依賴就會提高耦合的程度。
比較常見的就是 API 相關的行為,假設我們有一系列查詢社群網站貼文的功能。
1class FacebookPost
2 def initialize
3 @api = Facebook::Client.new(...)
4 end
5
6 def find(id)
7 @api.posts.find(id)
8 end
9end
10
11class TwitterPost
12 def initialize
13 @api = Twitter::Client.new(...)
14 end
15
16 def find(id)
17 @api.posts.find(id)
18 end
19end
這個例子我們的 FacebookPost
和 TwitterPost
各自跟對應的 API 客戶端耦合,當我們要進行測試時,就需要像這樣處理。
1RSpec.describe FacebookPost do
2 # ...
3
4 describe '#find' do
5 # ...
6 before do
7 posts = instance_double(Array)
8 post = instance_double(Facebook::Post)
9 api = instance_double(Facebook::Client, posts: posts)
10
11 allow(posts).to receive(:find).with(1).and_return(post)
12 allow(Facebook::Client).to receive(:new).and_return(api)
13 end
14
15
16 it { is_expected.to include(/Fake Content/) }
17 end
18end
像這樣,我們需要去定義非常多測試替身相關的處理才能夠驗證這件事情,假設我們希望統一抓取到社群網站的介面,情況又會更加麻煩。
1class SocialPost
2 def initialize(type = :facebook)
3 @service =
4 if type == :facebook
5 Facebook::Post.new
6 else
7 Twitter::Post.new
8 end
9 end
10
11 def find(id)
12 @service.find(id)
13 end
14end
像這樣的狀況,我們除了會有非常多判斷之外,還需要在測試時建構原本 Facebook::Post
測試相關的依賴才能夠正確的測試,最後測試就會變得越來越複雜。
依賴注入
想要解決這類問題,善用依賴注入(Dependency Injection)和設計模式(Design Pattern)來進行重構,某種程度上就能夠改善這樣的狀況。
以上面的例子來看,因為 Facebook::Client
和 Twitter::Client
的行為都是 @api.posts.find(id)
的,我們可以直接將這兩個物件當作是 SocialPost
的依賴,直接「注入」到裡面。
1class SocialPost
2 def initialize(source)
3 @source = source
4 end
5
6 def find(id)
7 @source.posts.find(id)
8 end
9end
如此一來我們除了可以消除 FacebookPost
和 TwitterPost
這兩個物件,還能夠將多餘的判斷消除,在撰寫測試也不需要先製作 FacebookPost
替身,再製作 Facebook::Client
替身,而是直接將 Facebook::Client
的替身作為依賴傳入即可。
另一方面,假設 Twitter 使用的是 @api.tweets.find(id)
而不是 @api.posts.find(id)
的話,就可以利用 Adapter 這類設計模式,我們先統一介面再來處理這些問題。
1class Adapter
2 def initialize(api)
3 @api = api
4 end
5end
6
7class FacebookAdapter < Adapter
8 def find_post(id)
9 @api.posts.find(id)
10 end
11end
12
13class TwitterAdapter < Adapter
14 def find_post(id)
15 @api.tweets.find(id)
16 end
17end
18
19# ...
20class SocialPost
21 def initialize(adapter)
22 @adapter = adapter
23 end
24
25 def find(id)
26 @adapter.find_post(id)
27 end
28end
調整之後,我們對 Adapter
的測試就是注入 Facebook::Client
或者 Twitter::Client
來驗證,而 SocialPost
只需要製作一個符合 Adapter#find_post
的替身來驗證後續的行為即可,這樣也讓我們測試的前置準備變得相對單純許多,也讓我們更容易擴充、修改和測試。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用優雅的 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 測試 - 探索式的測試與重構