Xcode 單元測試 (Unit Test 和 UI Test)

單元測試也是寫程式重要的一環,雖然我之前寫程式也沒有完全做到,但基本上的測試總是要有的,所以還是了解了一下 Xcode 上的單元測試。

我的環境是 macOS Catalina 10.15 + Xcode 11.1

建置專案

首先,在建立專案是要記得勾選「Include Unit Tests」和「Include UI Tests」。

image

如果一開始沒有加,也可以在主選單用 File -> New -> File... 加上去。

image

建好的專案就會像下圖一般,有 Unit Tests 和 UI Tests 二個區塊。

image

規劃程式

要測試的是一個簡單的類別,就是給二個整數,要取得二個整數和。

程式碼如下:

 

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())"

}

 

執行後的畫面如下:

image

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.

        }

    }

}

 

有幾件要先了解的事項。

  1. setUp() 是每一個測試模組要執行前會先執行的部份。

  2. tearDown() 是每一個測試模組執行後要執行的。

  3. testXXX() 就是我們的測試模組,一定要用 test 開頭。

  4. 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(...)

    }

 

只要按下紅色箭頭就會執行這一個測試模組了。

image

測試成功就會打一個綠色圖示。

image

底下改了一行,看一下失敗的情況,最前面變成紅色的圖示。

XCTAssertNil 是需要一個 nil,但 val 不是 nil,所以就出錯了。後面也有我們提供的錯誤訊息「此處要 nil」。

image

這些判斷式可查詢 [官方文件]

測試二:測試按下按鈕

這個測試是想模擬使用者按下了按鈕,標籤會不會變成 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"。

執行也是按下圖中的菱形圖示。

image

測試成功後,會出現綠色的圖示。

image

測試三:效能測試

若要測試效能,就把測試項目放在 testPerformanceExample() 的 self.measure 區塊中。

如下圖,就是把 add() 執行 10000 次,完成後會列出花費的時間。

image

全部測試

除了之前提到的執行測試方法,圖中紅色箭頭的地方都可以單獨執行。而綠色箭頭則可以全部執行。

image

全部執行後,看到一片綠,豈不是令人很放心嗎?! :D

image

Code Coverage

在研究別人網頁的說明時,有看到 Code Coverage 的圖表,覺得很有趣,但卻找不到在哪裡?查了線上說明,才知道開啟的方法。

主選單 -> Product -> Scheme -> Edit Scheme...

看到底下畫面後,左邊選 Test,右邊的 Code Coverage 將它勾選,並按下最下方的 Close (圖中沒有截到)。

image

接著重新跑一次測試,然後按下紅圈 1 的 Report navigator,再按下紅圈 2 的 Coverage。

此時右方就會出現每一個函式執行的情況,以及整體測試的覆蓋範圍。

image

另外,由主選單的 Editor 下拉後勾選 Code Coverage

image

就會在程式碼後面看到一排數字,聽說就是測試時執行某個函式的次數,細節我還沒深入了解。

image

UI Tests

UI Tests 是測試界面的操作,在前面的「測試二:測試按下按鈕」就可以用 UI Tests 來測試看看。

原本就有的程式碼我們就不去更動它。

這裡直接利用原本就有的 testExample() 來測試,如下圖,將游標移到綠色箭頭那一行,按下左下角紅色箭頭所指的紅圈,那個是「錄製」的功能,可以將使用者的動作錄下來,轉成程式碼。

image

按下紅色的錄製之後,就會執行程式。

我們先按下 Button,可以看到標籤的內容變成 20。

然後在 20 也點一下滑鼠。

要停止錄製時,就再按下左下角紅色箭頭的紅點,就會停止了。

image

此時畫面上已經有三行程式碼,就是我們按下 Button 和 20 的動作:

 

let window = XCUIApplication().windows["Window"]

window.buttons["Button"].click()

window.staticTexts["20"].click()

 

這時執行測試,就會看到程式開啟,並且模擬我們按下 Button 和 20 的標籤,然後報告測試成功。

此時我們把程式改一下,把 add() 改成多加 1。

image

再次執行測試,標籤變成 21,此時測試找不到 20 的標籤,就出錯了。

image

補充

測試的方法除了看畫面執行,應該也有不同的檢驗方法。

例如判斷 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。

image

 

image

再次錄製,先按新的 Button,按舊的 Button,再按 Label,可看到程式如下:

image

這裡它使用了 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")

 

 

重要度:
文章分類:

發表新回應

借我放一下廣告