iOS XCTest 單元測試與 UI 測試教學
寫測試是保證 App 品質的重要手段。Xcode 內建了強大的測試框架 XCTest。
環境建置 (Setup)
在建立專案時,通常勾選 Include Tests 就會自動建立測試 Target。如果你是事後想補測試:
- Xcode 選單 > File > New > Target...
- 搜尋 Unit Testing Bundle 或 UI Testing Bundle。
- 建立後,你會在專案導航欄看到對應的 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),並讓你在重構程式碼時更有信心。