初探 iOS 的单元测试(Unit Test)

3,648 阅读7分钟

就像上面這張圖一樣,當我們的項目開始變得複雜以後,有時會在增加功能的同時伴隨著 Bug 的產生。

而對於已經上線的產品,我們又希望更新的版本不要將 Bug 帶到使用者的面前,所以我們每次 release 前都會儘量地去做 test,
在項目還小的時候,我們可能是通過人為的操作介面來看常用場景是否有問題,而這個測試過程也是重複的。

所以還是想回到通過程式來進行測試,這次就先從 Unit Test開始吧。

初步使用Unit Test

引入測試框架

在剛開始建立Xcode Project的時候,系統會問要不要加入Test相關的內容,如果創建的時候沒有加入,可以在「File -> New -> Test -> UI Test/ Unit Test。

建立後會得到一個繼承 XCTestCase 的檔案。

需要注意的是,在這個類別中定義的測試方法,名稱需要以「test開頭」,並且「不返回內容」。
被認可的測試方法,左側會有個方塊,用來顯示測試是否成功。

做一個簡單的測試,可以看到左側會有測試結果。

Assert – 功能測試(1)

我們這裡做一個應用,在 HomeViewController 中,使用者可以通過 Slider 來改變 Key 1 以及 Key 2 的值( Slider min value 為 0 max value 為 10),
當 Key1 為 3 並且 Key2 為 7 的時候,會自動解鎖,並且push另外一個畫面進來。

我們這裡寫了一個方法「isUnlockSuccess()」用於判斷是否成功解鎖:

    func isUnlockSuccess(number1: Int, number2: Int) -> Bool {
        if number1 == 3 && number2 == 7 {
            return true
        } else {
            return false
        }
    }

那麼我們來為 isUnlockSuccess 方法寫一個測試。

我們的 Project 名稱是 Lab-Testing,而UnitTest文件名為Lab_Testing_UnitTest.swift。

我們通過引入 Lab_Testing 項目(我們項目名稱本來是 LabTesting,但似乎引入的時候只能寫 Lab_Testing)來取得HomeViewController並初始化。

@testable import Lab_Testing
 
class Lab_Testing_UnitTest: XCTestCase {
    var vc: HomeViewController!
    
    override func setUp() {
        super.setUp()
        // 測試開始前會執行這裡
        vc = HomeViewController(nibName: "HomeViewController", bundle: nil)
    }
}

接著我們針對 isUnlockSuccess 來寫一段 Test,記得要以 test 開頭,接著可以 comment + U 或者點 function 左側的方塊來跑測試,
成功後方塊就會變成綠色的勾勾了。

Assert – 功能測試(2)

還是同樣的應用,但我們要加入一個功能,從新畫面回來的以後,要恢復到初始狀態。

於是我們在 HomeViewController 中的 viewDidAppear 中加入 resetSettings 這個方法來初始化內容:

  • 將 key1Value、key2Value 還原成0。
  • 將 slider1、slider2 也回到初始位置(value 為0)
  • 將 Slider 右側 label 的 text 也顯示為 0 (初始化為0)
  • 並且將圖片換回「未解鎖」的圖片。

    func resetSettings() {
        key1Value = 0
        key2Value = 0
        
        lockImageView.image = UIImage(named: "icon-lock")
        
        slider1?.setValue(Float(key1Value), animated: true)
        slider2?.setValue(Float(key2Value), animated: true)
        
        key1Label?.text = "\(key1Value)"
        key2Label?.text = "\(key2Value)"
        
        unlockSliders()
    }

對應的測試應該這樣寫:

    func testResetSettings() {
        vc.resetSettings()
        
        XCTAssert(vc.key1Value == 0, "key1Value is not 0 after resetSettings")
        XCTAssert(vc.key2Value == 0, "key2Value is not 0 after resetSettings")
        
        XCTAssert(vc.key1Label.text == "0", "key1Label is not 0 after resetSettings")
        XCTAssert(vc.key2Label.text == "0", "key2Label is not 0 after resetSettings")
        
        XCTAssert(vc.lockImageView.image == UIImage(named: "icon-lock"), "image is not icon-lock after resetSettings")
    }

我們在 Lab_Testing_UnitTest 中有先宣告:

var vc: HomeViewController!

並且在 func setUp() 中有初始化它:

    override func setUp() {
        super.setUp()
        vc = HomeViewController(nibName: "HomeViewController", bundle: nil)
    }

需要特別注意的是,這樣的初始化馬上進行 test 會直接 crash,因為此時的 label, slider 等等,都因為他們都還沒有被初始化,而單元測試也不會觸發 loadView() ,所以我們需要主動去觸發初始化的動作,但 Apple 卻不希望我們直接調用 LoadView 方法(參考1參考2),所以我們通過調用vc.view的方式處理了:

    override func setUp() {
        super.setUp()
        // 測試開始前會執行這裡
        vc = HomeViewController(nibName: "HomeViewController", bundle: nil)
        _ = vc.view
    }

其他的Assertions

  • XCTAssertEqual
  • XCTFail
  • XCTAssertEqual
  • XCTAssertNil /  XCTAssertNotNil

請參考官方文件

Measure – 性能測試

我們可以通過Measure Block來進行性能測試,比如對 testIsUnlockSuccess() 中加入性能測試:

加入 measure 以後並跑完測試以後,右下角會顯示性能測試的結果,如果點開可以看到:

在這裡通過 edit 可以設定一個 baseline 以及允許的偏差值,如果運行結果超時就會跳出錯誤提醒。

Expection – 異步測試

一個網路請求的例子:

    func testUrlRequest() {
        let url = URL(string: "https://ios.devdon.com/")!
        let urlExpectation = expectation(description: "GET \(url)")
        
        let session = URLSession.shared
        let task = session.dataTask(with: url) { data, response, error in
            XCTAssert(data != nil, "data 不應該是 nil")
            XCTAssert(error == nil, "data 應當是 nil")
            
            if let response = response as? HTTPURLResponse,
                let responseURL = response.url {
                XCTAssert(responseURL.absoluteString == url.absoluteString, "URL變了")
                XCTAssert(response.statusCode == 200, "response code 不是200")
            } else {
                XCTFail()
            }
            
            urlExpectation.fulfill()
        }
        
        task.resume()
        
        waitForExpectations(timeout: task.originalRequest!.timeoutInterval, handler: { error in
            if let error = error {
                print("網路請求時發生錯誤: \(error.localizedDescription)")
            }
            task.cancel()
        })
    }

代碼覆蓋率(Code Coverage)

XCode 近幾年一直在不斷的更新,測試方面也是越來越方便了,現在的XCode可以幫我們生成代碼覆蓋率的資料,
我們只需要去 Scheme -> Test -> 勾選Code Coverage。

在跑過一輪測試以後,可以在下圖中看到代碼覆蓋率結果,可以看到我們其實還有很多地方沒有測試到。

相關資料可以參考官方文件

命令行測試(Command Line Testing)

為了更方便地進行測試,官方提供了通過 command line 來進行測試的方法,這樣我們可以通過撰寫 script 來實現自動化測試。

一個簡單的例子,我們指定了測試的項目、Scheme、

xcodebuild test -project Lab-Testing.xcodeproj -scheme Lab-Testing -destination 'platform=OS X,arch=x86_64'

有關更多命令行測試的內容,也請參考官方網站的資料。

在瞭解了基本的測試方法後,我們可以搭配 Jenkins 來實現更多的自動化測試方法


參考資料: