#-3 Xcode Unit Test

春麗 S.T.E.M.
11 min readJun 28, 2021

--

目錄
⦿ 測試APP
⦿ Unit Test
⦿ 建立一個簡單的頁面操作
⦿ textExample
⦿ XCTAssert
⦿ 其他Assert
⦿ 從頭加入測試
⦿ 再來個簡易示例
⦿ testPerformanceExample

測試APP

我們在 Xcode 寫完一個 APP 後會怎麼去測試它呢?Build。

在採用 SwiftUI 編寫 APP 時,我們通常會一邊觀察 Preview 的變化,一邊寫 Code,寫完後,我們還是需要 Build,尤其是在實機上。

雖然在 UIKit 也有辦法去觀察 Preview,但不是今天的重點。

繼續閱讀|回目錄

Unit Test

一個完整的 APP 並非簡單建置幾個頁面就完成,所以我們需要 Build ,但與 Build 不同的,若我們只想測試其中一個頁面的功能時,應該怎麼辦呢?

或者對於重複的測試操作已感厭煩的我們,又該怎麼辦呢?

難道要修改程式碼,Build,再修改程式碼,再 Build⋯⋯(雖然 lldb 的指令可以解決這種繁瑣,不過仍不在此次討論範圍內)

這時候,我們可以使用 Xcode 內建的 Unit Test 功能。

繼續閱讀|回目錄

建立一個簡單的頁面操作

首先,我們在 Main.storyboard 中拉了一個 button、一個 textfield、一個 label,按下 button 計算內部編碼給予的數值(相加)。

左為storyboard;右為simulator

由於 storyboard 預設與 ViewController.swift 繫結,所以這三個 UI元件 自然也要在 ViewController 裡與 UI class 繫結,才能在 viewDidLoad( ) 控制。

接著看到按下 button 的 action 裡用到了 lblText.text,而 lblText 從屬於 UITextField 這個 class,即是上方繫結的 textField,按下 button 後,text 會改變。

裡面宣告的 obj 是 Sample 這個 class,有兩個參數 12 跟 8,由此推斷 Sample 有個 add( ) 方法,後面才有 obj 用 dot syntax 呼叫了 add( ),如下:

Sample是寫在ViewController這個class外面的class
繼續閱讀|回目錄

textExample

寫完基本頁面操作後,接著,我們跳到 Unit Test 頁面

Unit Test標準頁面

這邊分別看到兩個覆寫方法跟兩個方法,setUpWithError( )、tearDownWithError( )、testExample( )、testPerformanceExample( ) 作用如下:

  1. setUpWithError( ):測試前要執行的,例如你宣告了 sop = URLSession
  2. tearDownWithError( ):測試後要執行的,例如將 sop = nil
  3. testExample( ):你要測試的方法,它的 prefix 必須是 test 開頭,在單元測試裡我們就寫了一個 testAdd( ) 方法。
  4. testPerformanceExample( ):效能測試,當我們以快捷 ⌘ + u 去做圖中的三個測試( testAdd、testExample、testPerformanceExample ),可看到左邊會有測試成功 ✅ 的圖案,整體 class 左邊也有一個測試成功 ✅ 的圖案,而 testExample( ) 沒東西暫時不用管它,但測試效能在 measure 的地方出現了灰色警示,提示執行時間沒有基準可參照
繼續閱讀|回目錄

XCTAssert

接著看 testAdd( ) 裡面的 XCTAssert。

XCTAssert 是用來判斷有沒有錯誤,透過官方文件,XCTAssert 的內容是 Asserts that expression is true.(假定表達式正確)

表示兩個參數一個回傳布林值, 一個回傳字串值
詳細的參數內部設定

如果 expression 為 true,就不會有後面的錯誤提示,即是 message;反之,false 就會在 debug 面板上顯示出 message。

前面我們知道了 Sample 這個 class 與它的方法,接著就可以丟給 XCTAssert 來做判斷。

obj 調用方法賦值給 val,val 即為 55 跟 45 相加(等於 100),所以 XCTAssert 裡:若 expression 錯誤,會告訴你 “ 相加應該要是 100 ” 。

繼續閱讀|回目錄

其他Assert

我們再看到 XCTAssertTrue 跟 XCTAssertFalse。

XCTAssertTrue 是你的 expression 需屬實,XCTAssertFalse 是你的 expression 需屬虛,如此才會是正確操作。

另有一個 XCTFail,XCTFail 是無條件失敗,直接給出錯誤訊息,前面並沒有將 XCTFail 寫入測試中,因為這樣無論如何都不會有 ✅ 。

然而,在 Xcode 裡面,為什麼需要有無條件失敗這一項,有的時候可以把它看成中斷點,有的時候則是希望提前發生錯誤,即是在錯誤之前的錯誤,這都是為了抓出錯誤的原因。

剩下的 XCTAssert:

  1. XCTAssertNil、XCTAssertNotNil,Nil 必須給它 Nil,無值,NotNil 必須給它非 Nil,有值,同樣在錯誤時才拋出錯誤訊息。
  2. XCTAssertEqual、XCTAssertNotEqual,Equal 是第一、第二參數必須相等,NotEqual 是第一、第二參數必須不相等。例如在 Equal 中,val, 100 表示 val == 100,同樣在錯誤時才拋出錯誤訊息。

測試時,一定得整頁程式碼由上而下,逐一測試嗎?

不,如果你有多個方法要個別測試,只要按下該方法旁邊的 start,比方說我現在只想測試 testAdd( ),就按下 test 旁邊的 start,此時測試正確的話就會出現 ✅,測試錯誤的話就出現 ❌。

注意!
雖然方法可以個別測試,但最上層的 Unit Test 的整體測試,必須在內部所有方法測試正確後才會顯示正確(即出現 ✅)。

繼續閱讀|回目錄

從頭加入測試

接著回到建立專案( Project )之初,下方就會問你要不要加入 Test,早先版本的 Xcode 會分別問你是要加入 Unit Test 還是 UI Test,會有兩個格子需要分別打勾,如今只剩下一個。

順帶複習一下,建立專案時,如果 Interface 選擇 Storyboard,Life Cycle 則只有 UIKit App Delegate,Language 處可選 Swift 或 OC;若 Interface 選擇 SwiftUI,Life Cycle 則多了一個 SwiftUI,Language 只剩 Swift 可選。

簡而言之,要用 UIKit 來編寫的話使用 Storyboard;要用 SwiftUI 來編寫的話使用 SwiftUI framework(SwiftUI 框架),Language 只能是 Swift。

接著,如果當初建立專案時忘了加入 Tests,在專案底下,我們可以從 File => New => Target 依序加入以下:

當然,我們也可以在 Xcode 的 Navigator(左邊欄位)右鍵 New File,加入Cocoa Touch Class,Subclass of XCTestCase 來加入另外的測試。

不過這個另寫的測試,在測試前,別忘了在程式碼最上方 import Model,像 @testable import TestTest(你另寫的測試檔案的名稱),至此,Unit Test(單元測試)都是在呼叫方法,並測試方法是否正確,沒有與 UI 連動。

繼續閱讀|回目錄

再來個簡易示例

我們再創建兩個數字的 加減乘除 共四個方法,分別測試是否正確。

也可以在 ViewController 裡寫方法,而不是新的 class 裡,於是,單元測試裡就要稍微調整,先寫方法,從加法開始

寫在ViewController class裡的方法

接著在 setUpWithError( ) 裡初始化

寫在Unit Test裡的初始化

這段 Code 相當簡單,就是實例化一個 vc,並轉型為 ViewController,於是這個 vc 就可以再去呼叫前面寫在 ViewController 裡的 plus。

再把它寫進測試方法裡,如下:

Unit Test裡,分別測試兩個方法,一個為正確,一個錯誤並丟出message。

我們看到第一個方法的結果正確,所以出現 ✅;但第二個方法的結果錯誤,所以出現 ❌,後面也看到錯誤訊息:Result is not nil.

再來,我們還想測試在按下 button 後程式碼是否正確,如下:

Unit Test裡

從 vc 去呼叫前面的 btnRun,此時 sender 為 self,而在 XCTAssert 裡,從 vc 去找到 lblText 的顯示結果,由於 btnRun 是 12 + 8,結果不為 30。

不過,此時卻回報錯誤如下:

ViewController裡,按下原先設置的按鈕會發生什麼事

奇怪,vc 已經初始化了,從 vc 呼叫方法,檢查 label 是否正確,這裡卻說 label 是 nil。

注意!
這不是 Unit Test 的錯誤。

上網查後才發現,原來還必須初始化 view,而蘋果官方不建議在 Unit Test 裡 loadView( ),因為可能導致 memory leaks

我們回到 setUpWithError( ) 加上一行:_ = vc.view ,表示所有的 view 都被初始化,這時候 label 就不會是 While implicitly unwrapping an Optional value(隱性解包)but found nil (卻無值)了。

補充:使用 _(底線)做為變數名稱,原因在於當你不會使用這個變數,就不要佔用名稱的空間。

那麼 _ = vc.view 就是告訴程式存取與觸發視圖

Unit Test裡

如此便可測試了,如下:

Unit Test裡

這下便跑出正確的單元測試錯誤,是不是很棒呀?

繼續閱讀|回目錄

testPerformanceExample

最後來講效能測試,testPerformanceExample( )。

Unit Test裡

我們試著加入一段測試,呼叫 add( ),讓 1 ~ 5000000 去加 8,幾番測試後,設定 baseline(效能基準時間)。

可以看到 baseline 設定為 1.98 s,接著跑十次做平均,平均 1.99 s,比baseline 還要差一個百分比( 1% worse)。

另外還有 Code Coverage,可以看到各個方法執行時的覆蓋率先不研究了,因為它似乎有點謎。

如此,單元測試的部分便完成了,感謝您的閱讀。

繼續閱讀|回目錄

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet