重構的時候我們需要一個模具,讓我們能夠大膽修改的同時確保結果的正確性,這個時候就要引入“單元測試”了。
前言
本文沒有給出任何測試程式碼,或者是在教你如何編寫一份具有良好測試性的程式碼,而是闡述在重構過程中單元測試的重要性與實現方法,關於程式碼可測試性相關的內容我會另開一篇文章去具體闡述。(畫個餅)
一、為什麼要引入單元測試
在開發過程中我們會遇到這樣一些問題:
- 面對需要重構龐大的模組程式碼時無從下手
- 修改了一處地方卻在另一處地方引發了新的bug
- 擴充套件新功能的同時導致舊程式碼出現bug
- 在測試人員難以覆蓋到的基礎功能介面出現了bug
- 出現了一種難以重現的特殊邊界條件觸發的bug
另外我們也許還會遇到一些這樣的模組:
- A模組依賴於B模組的結果,但是B模組尚未開發完成
- 模組狀態過於複雜,手工測試需要耗費大量時間
- 模組業務與時間節點相關,手工測試難以覆蓋
這個時候也許能夠利用經驗和豐富的debug技巧來解決這些問題,但是很多時候我們的處理並不完美,因為我們缺少了一個規範,在編碼過程中難以顧及其他模組的影響,這個時候,我們就需要引入單元測試。
二、單元測試的價值
可維護性增強
當在對程式碼進行修改時,利用單元測試就能夠清晰的知道是否破壞了老的業務邏輯,這樣大大減少了迴歸出錯的可能性。而當我們從測試那裡獲得了一個bug時,就可以通過測試用例去還原,當我們這個測試通過後,這個bug也就解決了,而且這個bug fix的測試用例也保證了這個bug以後不會再次出現。
降低重構難度
有了單元測試的保障,我們可以比較大膽的進行重構設計,而單元測試也會成為重構時很好的一個模具。當然在重構時也需要對單元測試進行重構,但是和可靠性相比,這種額外的負擔是值得去承受的
減少除錯時間
在除錯中,我們很多時候都需要花費一些額外的時間來觸發需要除錯的程式碼,但是在單元測試中,我們能夠針對需要除錯的程式碼構建相關的測試用例,方便的進行反覆的測試與模擬,大大減少了除錯的時間。
減少低階錯誤
測試的存在價值就是為我們發現並解決錯誤,單元測試更是如此,當我們對自己的程式碼進行單元測試時,就能容易的排除掉一些非常低階的錯誤,起碼我們能夠保證在一些正常的情況下程式碼是可以正確工作的。
描述程式碼
好的程式碼就是一份好的文件,單元測試更是如此。一份好的單元測試能夠描述在對應的情況下,程式碼應該有如何的預期表現,那麼別人只需要檢視測試用例就能清楚的知道程式碼的功能。
提高程式碼質量
一份程式碼如果和其他程式碼強耦合,它是難以被測試的,所以為了測試,開發人員會被驅使寫出低耦合、可擴充套件的程式碼。
三、單元測試方案
單元測試中有測試驅動開發(TDD)與行為驅動開發(BDD)兩種思路
測試驅動開發(TDD)
- 根據需求與介面先編寫測試用例
- 根據測試用例編寫業務程式碼
- 開發效率低
- 資源耗費大
- 測試覆蓋率高
行為驅動開發(BDD)
- 通過測試用例描述程式碼行為
- 通過自動執行測試用例快速反饋
- 通過Mock作為相關程式碼模組的替身
- 開發效率較高
- 資源耗費較低
- 測試覆蓋率較TDD要低
基於目前專案的情況與開發流程,我選擇了BDD作為測試框架,並會選擇使用XCTest + OCMock + OCHamcrest的方案,以下是三個框架的介紹:
XCTest
XCTest 可以完成的事
- 基本斷言的邏輯判斷
- 非同步測試
- 效能測試
為什麼選擇 XCTest
- XCode原生的測試框架,能夠更好適應Apple之後的更新
- XCTest有大量文件支援,上手難度較低
- XCTest新增進專案後只是作為專案測試框架,並不會影響到打包等一些東西
OCMock
為什麼需要 OCMock
mock即為模擬,OCMock可以偽造(模擬)一個物件,給它一些預設的值之類的,並進行對應的驗證
比如在我需要測試WiFi直連模組時,我需要一個WiFi才能測試直連功能,這個時候我們就可以利用OCMock,去模擬一個WiFi物件,它可以是模擬成風險WiFi,也可以模擬成免費WiFi,這樣我們的直連模組的測試就完全獨立於WiFi物件,可以方便的進行測試。
OCMock 可以完成的事
- 建立一個模擬物件,模擬一個特定物件的行為,排除一些外部類的干擾
- 構造自己的用例進行驗證
- 對已有方法進行重定義,以自己定義的邏輯進行互動
- 判斷函式是否執行過
為什麼選擇 OCMock
- 原生XCTest並不支援Mock功能
- OCMock是專門為iOS與OS X進行Mock測試的開源專案,擁有超過5000+ app使用,1100萬+下載量
- OCMock使用Apache 2.0協議,能夠在需要時候修改程式碼滿足需要並作為開源或商用產品釋出/銷售
- OCMock有官方文件,資料齊全
OCHamcrest
OCHamcrest 可以完成的事
- 更高階的斷言
- 斷言可擴充套件性
- 支援結構體的斷言
為什麼選擇 OCHamcrest
- 相對於另一個斷言框架 Expecta ,OCHamcrest更為成熟,Expecta可能會導致斷言結果錯誤
- XCTest 內建斷言並不充分,複雜條件下的判斷需要編寫大量斷言程式碼
- 利用OCHamcrest的擴充套件效能夠將格式化的自定義log輸出到日誌檔案,提供更多可以定製化而且詳細的資訊
四、整體測試框架
五、應該測試什麼 應該怎麼測試
測試的原則
- 快速:這樣才不會介意去執行
- 獨立:一個測試不應該耦合於另一個測試
- 可重複:每次測試的結果應該一致
- 可驗證:結果應該是成功/失敗,而不是一個解釋性的日誌文件
測試的內容
在寫任何測試前,應該明確應該要測試什麼,一般的情況下,單元測試應該包括這些內容:
- 核心功能測試
- 邊界條件
- 錯誤處理
測試的思路
針對目前專案情況,我會使用單元測試與人工測試相結合的方式去進行,因為目前我們大部分功能都是與UI牽連,不能完全依靠單元測試去完成所有的測試工作,但是我們可以將邏輯部分進行分離,舉網路連線模組為例:
請求流程
我們可以對介面相關的部分測試進行拆解,在邏輯部分實現單元測試,而人工測試部分單純檢查整個直連流程和介面部分是否正常。
這樣能夠避免人工測試時依賴於邏輯的情況,比如在需要測試發起100個請求後模組是否會出現問題時,無需依靠手工去真的連線100次,只需要在單元測試中模擬進行100次連線,並檢視結果是否正確就可以。
應該測試的物件
在專案中,我們有大量的類,全部覆蓋單元測試是不現實的,我們需要進行挑選。以下是我列舉的一些因素。
1.資料相關
比如在本地資料儲存模組中,我們需要儲存不同的資料,這時候我們可以通過單元測試構造不同的測試資料進行儲存,檢視是否儲存成功,資料部分是單元測試最需要覆蓋的部分。
2.邏輯相關
比如在連線模組中,需要對部分請求結果進行過濾,這就是一個邏輯,針對這種邏輯,可以在單元測試中進行測試是否過濾成功,而人工測試則無需關注過濾的邏輯,僅僅需要關注過濾後介面是否正常顯示。
3.多狀態的模組
比如在連線模組中,連線的狀態就有8種,包括了連通性檢查、連線中、已連線等,這些狀態能夠利用單元測試很好的模擬出來,這樣就解決了人工測試下難以模擬不同狀態轉換的問題。
六、總結
我們寫程式碼最終的目的只有兩個:實現需求與提高程式碼質量,在保證完成需求的前提下,增加單元測試能提高程式碼的質量與可維護性,縱使在引入了單元測試後,我們也許會面臨增加了研發的程式碼量,花費更多精力在編寫單元測試上,增加了開發成本,但我認為相比於單元測試帶來的優勢,這些是能夠克服的。
更多內容可以關注我的部落格