测试金字塔
大量快而廉价的单元测试打底,少量集成测试居中,更少但更慢的 UI/端到端测试封顶。把重心放在单测,性价最高。
XCTest 单元测试
XCTest 是苹果原生测试框架。测试类继承 XCTestCase,方法名以 test 开头即被自动发现,用断言验证行为。
import XCTest
@testable import MyApp
final class CartTests: XCTestCase {
func test_add_increasesTotal() {
let cart = Cart()
cart.add(Product(name: "A", price: 10))
cart.add(Product(name: "B", price: 5))
XCTAssertEqual(cart.total, 15) // 相等断言
XCTAssertFalse(cart.items.isEmpty) // 布尔断言
}
func test_emptyCart_totalIsZero() {
XCTAssertEqual(Cart().total, 0)
}
}
测 async 代码用 await 配合 XCTAssertEqual(try await vm.load(), expected);需要等待异步完成的用 XCTestExpectation 并在回调里 fulfill()。
测试替身
用替身替换真实依赖:Stub 返回固定数据、Mock 还验证"是否被按预期调用"、Fake 是轻量可工作的实现。结合上一章的协议化网络层,单测完全不触网。
final class MockCartService: CartService {
var addCalled = false
func add(_ p: Product) { addCalled = true } // 记录调用
}
func test_viewModel_callsService() {
let mock = MockCartService()
let vm = CartViewModel(service: mock)
vm.addToCart(Product(name: "X", price: 1))
XCTAssertTrue(mock.addCalled) // 验证行为
}
UI 测试
XCUITest 在独立进程启动 App,通过 accessibility 标识符模拟用户点击,验证关键流程(如登录、下单)。慢但能抓端到端回归。
func test_loginFlow() {
let app = XCUIApplication()
app.launch()
app.textFields["username"].tap()
app.textFields["username"].typeText("林可")
app.buttons["登录"].tap()
XCTAssertTrue(app.staticTexts["欢迎,林可"].exists)
}
UI 测试启动真机级 App,分钟级耗时。只在 CI 跑核心链路,细节逻辑交给单测。给关键控件设稳定的 accessibilityIdentifier,避免靠文案定位导致脆弱。
Snapshot 快照测试
把视图渲染成图片,与基线比对。一次提交就能发现像素级 UI 回归,特别适合组件库与频繁改版的界面。
import SnapshotTesting
func test_cardView_snapshot() {
let view = CardView(model: .preview)
assertSnapshot(of: view, as: .image(layout: .fixed(width: 320, height: 200)))
}
快照依赖分辨率/字体/系统版本。固定测试设备与系统版本,或对浮点/动态字号敏感处做容差配置,避免"伪回归"。
XCTestPlan 与 CI
XCTestPlan 把测试分组、配置覆盖率和性能基线,配合 Xcode Cloud 或自托管 CI 在每次提交时自动跑,守住回归。
CI 是最后一道闸,不是第一道。养成"本地跑通相关测试再提交",把反馈循环缩到最短。
调试基础
断点是最常用武器:普通断点暂停执行,符号断点监听特定方法(如 UIViewAlertForUnsatisfiableConstraints 抓约束冲突),异常断点在崩溃处停下。
// 在 LLDB 里常用命令
// po view —— 打印对象描述
// bt —— 打印调用栈
// expr —— 在暂停时执行表达式(如改值看效果)
卡顿用 Time Profiler,内存用 Allocations/Leaks,电量用 Energy Log。下一章性能专题会深入。调试时先定位"是哪种问题",再选对应模板,别盲目猜。
常见陷阱与 FAQ
多半是测试中触碰了真实网络、数据库或启动了真 App 生命周期。用 Mock/Stub 替换外部依赖,把被测单元缩到纯逻辑,单测应是毫秒级。
单测只验证了逻辑层。视觉回归要靠 Snapshot 测试或人工走查;交互流程要靠 UI 测试。不同层次覆盖不同问题,没有哪一种能替代全部。
优先通过公共接口测行为,而不是直接测私有实现——后者会让测试与实现强耦合、难以重构。如果私有逻辑复杂到需要单独测,往往说明它该被抽成一个可独立测试的单元。