單元測試也是寫程式重要的一環,雖然我之前寫程式也沒有完全做到,但基本上的測試總是要有的,所以還是了解了一下 Xcode 上的單元測試。
我的環境是 macOS Catalina 10.15 + Xcode 11.1
建置專案
首先,在建立專案是要記得勾選「Include Unit Tests」和「Include UI Tests」。
如果一開始沒有加,也可以在主選單用 File -> New -> File... 加上去。
建好的專案就會像下圖一般,有 Unit Tests 和 UI Tests 二個區塊。
規劃程式
要測試的是一個簡單的類別,就是給二個整數,要取得二個整數和。
程式碼如下:
class Sample {
var a = 0
var b = 0
init (_ a: Int, _ b:Int) {
self.a = a
self.b = b
}
func add () -> Int {
return (a + b)
}
}
程式的界面也很簡單,一個按鈕,一個標籤,按鈕按下後,建立一個 Sample 物件,給二個整數 12 和 8,然後把 add() 的結果放在標籤上。
程式碼如下:
@IBOutlet weak var lbText: NSTextField!
......
@IBAction func btRun(_ sender: Any) {
let obj = Sample(12, 8)
lbText.stringValue = "\(obj.add())"
}
執行後的畫面如下:
Unit Tests
一個空白的單元測試如下,unittest 是專案名稱。
import XCTest
@testable import unittest
class unittestTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
有幾件要先了解的事項。
-
setUp() 是每一個測試模組要執行前會先執行的部份。
-
tearDown() 是每一個測試模組執行後要執行的。
-
testXXX() 就是我們的測試模組,一定要用 test 開頭。
-
testPerformanceExample() 是測試效能的。
測試一:測試 Sample 的 add() 函式
第一個測試就是要試看看 Sample 類別中的 add() 函式是否運行正確。
在單元測試程式中加入這一段,建立 obj 物件,給予 55 和 45 二個整數,理論中得到的結果 val 應該是 100。
程式中用了很多判斷,這是故意提供很多種判斷的方法,這些判斷式後面都可以加上字串當成提示訊息。
func testAdd() {
let obj = Sample(55, 45)
let val = obj.add()
XCTAssert(val == 100, "應該要 100")
XCTAssertNil(nil)
XCTAssertNotNil(val)
XCTAssertEqual(val, 100)
XCTAssertNotEqual(val, 101)
XCTAssertTrue(val > 99 && val < 101)
XCTAssertFalse(val != 100)
//XCTAssertNoThrow(...)
//XCTAssertThrowsError(...)
}
只要按下紅色箭頭就會執行這一個測試模組了。
測試成功就會打一個綠色圖示。
底下改了一行,看一下失敗的情況,最前面變成紅色的圖示。
XCTAssertNil 是需要一個 nil,但 val 不是 nil,所以就出錯了。後面也有我們提供的錯誤訊息「此處要 nil」。
這些判斷式可查詢 [官方文件]
測試二:測試按下按鈕
這個測試是想模擬使用者按下了按鈕,標籤會不會變成 20?
測試程式如下,只摘錄部份:
import XCTest
@testable import unittest
class unittestTests: XCTestCase {
var vc: ViewController?
override func setUp() {
let storyboard = NSStoryboard(name: "Main", bundle: Bundle.main)
let wc = storyboard.instantiateInitialController() as? NSWindowController
vc = wc?.contentViewController as? ViewController
}
func testPushButton () {
vc?.btRun(self)
XCTAssert(vc?.lbText.stringValue == "20")
}
紅色是宣告一個成員變數 vc 為 ViewController
藍色是初始化的部份,就是由 storyboard 取得 window 再取得 vc
紫色的 testPushButton() 就是我們要模擬按下按鈕的程式,就是去執行 btRun(self),再看看標籤的文字是不是 "20"。
執行也是按下圖中的菱形圖示。
測試成功後,會出現綠色的圖示。
測試三:效能測試
若要測試效能,就把測試項目放在 testPerformanceExample() 的 self.measure 區塊中。
如下圖,就是把 add() 執行 10000 次,完成後會列出花費的時間。
全部測試
除了之前提到的執行測試方法,圖中紅色箭頭的地方都可以單獨執行。而綠色箭頭則可以全部執行。
全部執行後,看到一片綠,豈不是令人很放心嗎?! :D
Code Coverage
在研究別人網頁的說明時,有看到 Code Coverage 的圖表,覺得很有趣,但卻找不到在哪裡?查了線上說明,才知道開啟的方法。
主選單 -> Product -> Scheme -> Edit Scheme...
看到底下畫面後,左邊選 Test,右邊的 Code Coverage 將它勾選,並按下最下方的 Close (圖中沒有截到)。
接著重新跑一次測試,然後按下紅圈 1 的 Report navigator,再按下紅圈 2 的 Coverage。
此時右方就會出現每一個函式執行的情況,以及整體測試的覆蓋範圍。
另外,由主選單的 Editor 下拉後勾選 Code Coverage
就會在程式碼後面看到一排數字,聽說就是測試時執行某個函式的次數,細節我還沒深入了解。
UI Tests
UI Tests 是測試界面的操作,在前面的「測試二:測試按下按鈕」就可以用 UI Tests 來測試看看。
原本就有的程式碼我們就不去更動它。
這裡直接利用原本就有的 testExample() 來測試,如下圖,將游標移到綠色箭頭那一行,按下左下角紅色箭頭所指的紅圈,那個是「錄製」的功能,可以將使用者的動作錄下來,轉成程式碼。
按下紅色的錄製之後,就會執行程式。
我們先按下 Button,可以看到標籤的內容變成 20。
然後在 20 也點一下滑鼠。
要停止錄製時,就再按下左下角紅色箭頭的紅點,就會停止了。
此時畫面上已經有三行程式碼,就是我們按下 Button 和 20 的動作:
let window = XCUIApplication().windows["Window"]
window.buttons["Button"].click()
window.staticTexts["20"].click()
這時執行測試,就會看到程式開啟,並且模擬我們按下 Button 和 20 的標籤,然後報告測試成功。
此時我們把程式改一下,把 add() 改成多加 1。
再次執行測試,標籤變成 21,此時測試找不到 20 的標籤,就出錯了。
補充
測試的方法除了看畫面執行,應該也有不同的檢驗方法。
例如判斷 20 的標籤是否存在
XCTAssert(window.staticTexts["20"].exists)
或是 20 的標籤內容是不是 20 (感覺有點多此一舉)
XCTAssert(window.staticTexts["20"].value as! String == "20")
程式中判斷 Button 是根據這行
window.buttons["Button"].click()
我測試了一下,若有二個按鈕的名稱都是 Button,程式會如何處理?於是增加了一個 Button,錄製程式後,分別按下去,程式變成如下:
window.children(matching: .button).matching(identifier: "Button").element(boundBy: 0).click()
window.children(matching: .button).matching(identifier: "Button").element(boundBy: 1).click()
還有一個方法,如下圖,分別在 Button 和 Label 的 Accessibility Identity 的 Identifier 填上 myButton 和 myLabel。
再次錄製,先按新的 Button,按舊的 Button,再按 Label,可看到程式如下:
這裡它使用了 Accessibility Identity 的 Identifier,就不會有重複的問題,程式也比較好看,至少不用去判斷這是第幾個同樣 title 的按鈕。
上圖中最後一行有奇怪的反白,此時把程式碼 copy 下來竟是如下:
window/*@START_MENU_TOKEN@*/.staticTexts["myLabel"]/*[[".staticTexts[\"20\"]",".staticTexts[\"myLabel\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.click()
我在反白處連點二下,反白才消失,再次 copy 程式碼就變成如下了:
window.staticTexts["myLabel"].click()
大概是這裡有二種選擇 Lable 的方法,根據內容 20 或 Identifier 為 myLabel,要讓我們自行處理吧?只是我也還不知如何選擇?
最後把測試碼改成如下,我個人覺得就是合理又好懂了。
let window = XCUIApplication().windows["Window"]
window.buttons["myButton"].click()
XCTAssert(window.staticTexts["myLabel"].value as! String == "20")
- 瀏覽次數:16356
發表新回應