運用 MethodSwizzling 和 OCMock 於 Objective-C Unit Test
老實說,Objective-C 裡寫 UT 還真不是件容易的事情。最近在公司的 project 裡用 category patch 一個既有的 Class,主要的目的是要加上 WebSocket 連線功能。因為是新功能,所以也順便來寫一些 unit test case 來測試。
UT framework 我們是採用 OCUnit,Mock object 則是採用 OCMock。一開始我先設計原有 class extend 出來的 API,例如:
-(void)openWebSocketConnectionForUrl:(NSURL*)anURL onSuccess:(CallbackBlock)successBlock onFailure(FailureBlock)failureBlock;
如果要去測試這個 function,會遇到兩個問題:
它是 async
它內部會使用 SocketRocket 去做 WebSocket 連線 (SRWebSocket)
如果是用 SenTestKit 作測試時,遇到 async 程式時,最大的麻煩就是,在你的 callback 還沒被呼叫時,程式就已經結束,然後該測的東西都沒測到,所以測試 async 必須要等待被測者被呼叫到:
你必須在現在的 RunLoop 裡等待一個合理的時間,當時間到時,如果你的 callback 都還沒執行到,那麼或許就是某的地方出錯,timeout 了。而等待一段時間也可以確保你的 callback 可以被 UT 程式驗證到。
再來,因為 SRWebSocket 是一個外部的物件,我們並不想對它做測試,需要被測試的是我們自己編寫的 business logic 部分,所以比較好的作法是把 SRWebSocket 變成一個 mock object,這個 mock 的任何動作都是我們測試程式可以依據測試需求所控制的。
所以,首先要把被測程式中所用到 SRWebSocket 物件至換成我們做出來的 mock object。再來要確保 mock object 不會被再次置換掉。第一個條件還簡單,因為被測程式的 SRWebSocket 是一個被 export 出來的 property,所以要換掉他只要透過該 property 的 setter 即可。
但是第二個條件就會有點麻煩,在我們被測程式中,一當 openWebSocketConnectionForUrl 函式被呼叫時,內部會自動去呼叫另外一個函式 reconnectWebSocket ,這個函式會將原有的 SRWebSocket 物件 release 掉並重新 create 一個,如此一來辛辛苦苦創建的 mock object 就不見了。所以為了確保這件事不會發生,用了一點小技巧,就是 MethodSwizzle,將原有的 reconnectWebSocket 函式置換掉,然後在新版的 reconnectWebSocket 中去使用我們所塞進去的 mock object。而為了做到這兩件事,我另外寫了一個新的 category 去 extend 原有的功能,並且在 +load 函式裡去做 MethodSwizzle 的工作。
在 Objective-C 裡,透過 Category 可以在 runtime 時替特定 Class 作 patch 以增加該 class 功能,category 不是在 compile time 時被 patch 進去的,而是在 runtime 時才做載入的動作。我們的程式沒辦法控制 category 在 runtime 何時會被載入,但是當一個 category 一被載入時,如果 +load 函式有被實作,該函式就會被呼叫。所以可以在 +load 函式裡去做函式置換的動作。
在這個 category 裡頭,我們替 WARemoteInterface 新增加了兩個函式,分別是 replaceWebSocketConnection 跟 reconnectWebSocket_override。第一個函式就是用來將 SRWebSocket 的 mock object 塞進我們的被測程式當中。而第二個函式就是我們意圖來替換原有的 reconnectWebSocket 函式。而在 +load 裡,透過 MethodSwizzle 的手法,將 reconnectWebSocket 的實作換成 reconnectWebSocket_override,如此一來,任何呼叫到 reconnectWebSocket 的動作其實都是呼叫到這個新函式。
最後,我們透過 OCMock 來製作我們的 Mock object,並且將這些都串起來 (底下只是程式碼片段):
透過 OCMock 的 partialMockForObject 可以讓我們從一個正常的物件製作出 Mock object。而透過 stub 函式可以讓我們對任何 mock object 裡頭的函式呼叫執行對應的 block 或者設定回傳值。以這個例子來說,由於 SRWebSocket 是透過 delegate 方式運作,所以我們也需要去呼叫被測程式的 delegate 函式,來讓它做原本就應該做的事情。
對於 Objective-C UT 有興趣的人,不妨留言或者來信討論,對於我們 app 有興趣的人可以到 AppStore 下載 Waveface Stream app。









