iOS 单元测试与 UI 测试详解
在 iOS 开发中,测试主要分为单元测试(Unit Test)和 UI 测试(UI Test),下面我将详细介绍如何实施这两种测试。
一、单元测试 (Unit Test)
1. 单元测试核心概念
单元测试是针对代码中最小可测试单元的测试,通常是测试单个函数或方法。
2. 如何编写单元测试
基础测试示例
swift
scss
import XCTest
@testable import YourApp
class MathTests: XCTestCase {
func testAddition() {
let result = Calculator().add(2, 3)
XCTAssertEqual(result, 5, "2 + 3 应该等于 5")
}
func testDivision() {
let calculator = Calculator()
XCTAssertEqual(calculator.divide(10, by: 2), 5)
XCTAssertThrowsError(try calculator.divide(10, by: 0))
}
}
测试模型层
swift
csharp
func testUserModel() {
// 准备测试数据
let userJSON = """
{
"id": 123,
"name": "John Doe",
"email": "[email protected]"
}
""".data(using: .utf8)!
// 测试解码
do {
let user = try JSONDecoder().decode(User.self, from: userJSON)
XCTAssertEqual(user.id, 123)
XCTAssertEqual(user.name, "John Doe")
XCTAssertEqual(user.email, "[email protected]")
} catch {
XCTFail("解码失败: (error)")
}
}
测试网络请求
swift
php
func testNetworkRequest() {
// 1. 创建模拟网络环境
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)
// 2. 准备模拟响应数据
let testData = """
{"status": "success", "data": {"id": 1, "name": "Test"}}
""".data(using: .utf8)!
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!
return (response, testData)
}
// 3. 创建期望
let expectation = XCTestExpectation(description: "Network request")
// 4. 执行测试
let networkManager = NetworkManager(session: session)
networkManager.fetchUser(id: 1) { result in
switch result {
case .success(let user):
XCTAssertEqual(user.id, 1)
XCTAssertEqual(user.name, "Test")
case .failure(let error):
XCTFail("请求失败: (error)")
}
expectation.fulfill()
}
// 5. 等待期望完成
wait(for: [expectation], timeout: 1)
}
3. 测试 ViewModel/Presenter
swift
scss
func testLoginViewModel() {
let mockService = MockAuthService()
let viewModel = LoginViewModel(authService: mockService)
// 测试初始状态
XCTAssertFalse(viewModel.isLoading)
XCTAssertFalse(viewModel.isLoggedIn)
// 模拟成功登录
mockService.loginResult = .success(true)
viewModel.login(username: "test", password: "123456")
// 验证状态变化
XCTAssertTrue(viewModel.isLoading)
// 使用期望等待异步操作
let expectation = XCTestExpectation(description: "Login completed")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertFalse(viewModel.isLoading)
XCTAssertTrue(viewModel.isLoggedIn)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
二、UI 测试 (UI Test)
1. UI 测试核心概念
UI 测试模拟用户与应用程序的交互,验证整个用户界面的行为是否符合预期。
2. 如何编写 UI 测试
基础 UI 测试示例
swift
scss
import XCTest
class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
// 定位界面元素
let usernameField = app.textFields["username"]
let passwordField = app.secureTextFields["password"]
let loginButton = app.buttons["loginButton"]
// 验证元素存在
XCTAssertTrue(usernameField.exists)
XCTAssertTrue(passwordField.exists)
XCTAssertTrue(loginButton.exists)
// 输入测试数据
usernameField.tap()
usernameField.typeText("testuser")
passwordField.tap()
passwordField.typeText("password123")
// 执行登录操作
loginButton.tap()
// 验证登录结果
let welcomeText = app.staticTexts["Welcome, testuser"]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5))
}
func testInvalidLogin() {
let usernameField = app.textFields["username"]
let passwordField = app.secureTextFields["password"]
let loginButton = app.buttons["loginButton"]
// 输入无效凭据
usernameField.tap()
usernameField.typeText("wrong")
passwordField.tap()
passwordField.typeText("wrong")
loginButton.tap()
// 验证错误提示
let alert = app.alerts["Login Failed"]
XCTAssertTrue(alert.waitForExistence(timeout: 2))
XCTAssertTrue(alert.staticTexts["Invalid username or password"].exists)
}
}
测试 TableView/CollectionView
swift
scss
func testTableView() {
// 确保表格存在
let tableView = app.tables["itemTableView"]
XCTAssertTrue(tableView.exists)
// 获取单元格数量
let cells = tableView.cells
XCTAssertGreaterThan(cells.count, 0)
// 测试第一个单元格
let firstCell = cells.element(boundBy: 0)
XCTAssertTrue(firstCell.exists)
// 点击第一个单元格
firstCell.tap()
// 验证详情页显示
let detailTitle = app.staticTexts["detailTitle"]
XCTAssertTrue(detailTitle.waitForExistence(timeout: 1))
}
测试复杂手势
swift
less
func testSwipeToDelete() {
let tableView = app.tables["itemTableView"]
let initialCount = tableView.cells.count
// 获取第一个单元格
let firstCell = tableView.cells.element(boundBy: 0)
// 向左滑动
let start = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5))
let end = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.5))
start.press(forDuration: 0.1, thenDragTo: end)
// 点击删除按钮
let deleteButton = app.buttons["Delete"]
XCTAssertTrue(deleteButton.waitForExistence(timeout: 1))
deleteButton.tap()
// 验证单元格数量减少
let newCount = tableView.cells.count
XCTAssertEqual(newCount, initialCount - 1)
}
3. UI 测试最佳实践
-
使用 accessibility identifiers 而不是文本内容定位元素
swift
less// 在代码中设置 button.accessibilityIdentifier = "loginButton" // 在测试中引用 app.buttons["loginButton"]
-
等待元素出现 而不是硬编码 sleep
swift
lesslet element = app.staticTexts["welcomeText"] XCTAssertTrue(element.waitForExistence(timeout: 5))
-
重置应用状态 在每个测试前
swift
scssoverride func setUp() { super.setUp() app.launchArguments.append("--uitesting") app.launch() }
-
处理系统弹窗 (如位置权限)
swift
javascriptaddUIInterruptionMonitor(withDescription: "Location Permission") { (alert) -> Bool in if alert.buttons["Allow"].exists { alert.buttons["Allow"].tap() return true } return false }
三、单元测试与 UI 测试对比
特性 | 单元测试 | UI 测试 |
---|---|---|
测试范围 | 单个函数/方法 | 完整用户流程 |
执行速度 | 快 | 慢 |
维护成本 | 低 | 高 |
稳定性 | 高 | 较低 |
适合场景 | 业务逻辑、算法 | 用户交互、界面流程 |
依赖 | 最小化依赖,使用mock | 完整应用环境 |
四、测试金字塔实践建议
- 大量单元测试 (70%) - 测试核心业务逻辑
- 适量集成测试 (20%) - 测试模块间交互
- 少量UI测试 (10%) - 测试关键用户流程
五、常见问题解决
单元测试问题
问题1 : @testable import
找不到模块
-
解决方案:
- 确保测试目标的"Enable Testability"设置为Yes
- 检查scheme设置是否正确
问题2: 异步测试不稳定
-
解决方案:
swift
scsslet expectation = XCTestExpectation(description: "Async test") someAsyncCall { expectation.fulfill() } wait(for: [expectation], timeout: 5)
UI 测试问题
问题1: 元素找不到
-
解决方案:
- 使用accessibilityIdentifier而不是文本
- 增加等待时间
waitForExistence(timeout:)
问题2: 测试在模拟器上运行慢
-
解决方案:
- 关闭动画
app.launchArguments += ["-UIAnimationSpeed", "2"]
- 使用更快的模拟器设备
- 关闭动画
通过合理组合单元测试和UI测试,可以构建全面的iOS应用测试体系,有效提高应用质量和开发效率。
from: deepseek