iOS XCTest 單元測試與 UI 測試教學

寫測試是保證 App 品質的重要手段。Xcode 內建了強大的測試框架 XCTest

環境建置 (Setup)

在建立專案時,通常勾選 Include Tests 就會自動建立測試 Target。如果你是事後想補測試:

  1. Xcode 選單 > File > New > Target...
  2. 搜尋 Unit Testing BundleUI Testing Bundle
  3. 建立後,你會在專案導航欄看到對應的 Tests 資料夾。

執行測試 (Running Tests)

有幾種方式可以執行測試:

  • 快捷鍵: 按下 Command + U 執行所有測試。
  • 點擊圖示: 點擊程式碼行號旁邊的菱形圖示 (Diamond Icon) 來執行單個測試方法。
  • Test Navigator: 在左側導航欄切換到 Test 分頁,選擇要跑的測試。

單元測試 (Unit Test)

單元測試針對 App 的最小功能單位(通常是 Model 或 ViewModel 的邏輯)進行驗證。

import XCTest
@testable import MyCalculator // 引入你的 App Target

final class CalculatorTests: XCTestCase {
    
    var calc: Calculator!
    
    // 每個測試開始前都會執行 (重置環境)
    override func setUp() {
        super.setUp()
        calc = Calculator()
    }
    
    override func tearDown() {
        calc = nil
        super.tearDown()
    }
    
    func testAddition() {
        // Arrange
        let a = 2
        let b = 3
        
        // Act
        let result = calc.add(a, b)
        
        // Assert
        XCTAssertEqual(result, 5, "2 + 3 應該等於 5")
    }
}

異步測試 (Asynchronous Testing)

當你要測試網路請求或其他異步操作時,普通的測試方法會失敗(因為程式還沒跑完,測試就結束了)。這時需要使用 XCTestExpectation

func testFetchData() {
    let expectation = XCTestExpectation(description: "Fetch Data Success")
    
    APIService.shared.fetch { result in
        switch result {
        case .success(let data):
            XCTAssertNotNil(data)
            expectation.fulfill() // 標記完成
        case .failure:
            XCTFail("請求失敗")
        }
    }
    
    // 等待 5 秒,如果沒 fulfill 就會失敗
    wait(for: [expectation], timeout: 5.0)
}

效能測試 (Performance Testing)

你可以測量一段程式碼的執行時間,確保它不會變慢。

func testPerformanceExample() {
    self.measure {
        // 這段程式碼會被執行 10 次,Xcode 會計算平均時間
        calc.heavyComputation()
    }
}

UI 測試 (UI Test)

UI 測試模擬使用者實際操作 App 的行為(點擊按鈕、輸入文字)。

final class UITests: XCTestCase {
    func testLoginFlow() {
        let app = XCUIApplication()
        app.launch() // 啟動 App
        
        // 找到輸入框並打字
        let emailField = app.textFields["Email"]
        XCTAssertTrue(emailField.exists)
        emailField.tap()
        emailField.typeText("user@example.com")
        
        // 點擊登入按鈕
        app.buttons["Login"].tap()
        
        // 驗證是否出現歡迎訊息
        let welcomeText = app.staticTexts["Welcome"]
        
        // 等待元素出現 (處理動畫或網路延遲)
        let exists = welcomeText.waitForExistence(timeout: 2.0)
        XCTAssertTrue(exists)
    }
}

Mocking (模擬依賴)

為了讓測試更穩定,我們通常會定義 Protocol 來模擬外部依賴(如網路層)。

protocol NetworkService {
    func fetch() async -> String
}

class MockNetworkService: NetworkService {
    func fetch() async -> String {
        return "Mock Data" // 固定回傳假資料,不真的發請求
    }
}

// 測試時注入 Mock
let vm = ViewModel(service: MockNetworkService())

寫測試雖然初期需要花費時間,但長遠來看,它能節省大量手動回歸測試的時間 (Regression Testing),並讓你在重構程式碼時更有信心。