iOS 开发手册IOS DEVELOPMENT

CHAPTER 08质量保障 · TESTING

测试与调试

没有测试,重构就是赌博;没有调试手段,Bug 只能靠猜。本章覆盖 XCTest 单元/UI 测试、Snapshot 快照、测试替身、XCTestPlan 与 Instruments 基础。

开始阅读 难度中级 先修架构 + 网络 约2小时
08.1

测试金字塔

大量快而廉价的单元测试打底,少量集成测试居中,更少但更慢的 UI/端到端测试封顶。把重心放在单测,性价最高。

UI / E2E(少) 集成测试(中) 单元测试(多、快)
越往底层越快越稳定,越多越好;越往顶层越慢越脆,越少越好。
08.2

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

08.3

测试替身

用替身替换真实依赖: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)        // 验证行为
}
08.4

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 测试很慢

UI 测试启动真机级 App,分钟级耗时。只在 CI 跑核心链路,细节逻辑交给单测。给关键控件设稳定的 accessibilityIdentifier,避免靠文案定位导致脆弱。

08.5

Snapshot 快照测试

把视图渲染成图片,与基线比对。一次提交就能发现像素级 UI 回归,特别适合组件库与频繁改版的界面。

import SnapshotTesting

func test_cardView_snapshot() {
    let view = CardView(model: .preview)
    assertSnapshot(of: view, as: .image(layout: .fixed(width: 320, height: 200)))
}
设备与系统差异

快照依赖分辨率/字体/系统版本。固定测试设备与系统版本,或对浮点/动态字号敏感处做容差配置,避免"伪回归"。

08.6

XCTestPlan 与 CI

XCTestPlan 把测试分组、配置覆盖率和性能基线,配合 Xcode Cloud 或自托管 CI 在每次提交时自动跑,守住回归。

本地先绿再提交

CI 是最后一道闸,不是第一道。养成"本地跑通相关测试再提交",把反馈循环缩到最短。

08.7

调试基础

断点是最常用武器:普通断点暂停执行,符号断点监听特定方法(如 UIViewAlertForUnsatisfiableConstraints 抓约束冲突),异常断点在崩溃处停下。

// 在 LLDB 里常用命令
// po view  —— 打印对象描述
// bt       —— 打印调用栈
// expr  —— 在暂停时执行表达式(如改值看效果)
Instruments 入门

卡顿用 Time Profiler,内存用 Allocations/Leaks,电量用 Energy Log。下一章性能专题会深入。调试时先定位"是哪种问题",再选对应模板,别盲目猜。

08.8

常见陷阱与 FAQ

单测为什么跑起来很慢?

多半是测试中触碰了真实网络、数据库或启动了真 App 生命周期。用 Mock/Stub 替换外部依赖,把被测单元缩到纯逻辑,单测应是毫秒级。

断言通过但 UI 还是错?

单测只验证了逻辑层。视觉回归要靠 Snapshot 测试或人工走查;交互流程要靠 UI 测试。不同层次覆盖不同问题,没有哪一种能替代全部。

测试要不要测私有方法?

优先通过公共接口测行为,而不是直接测私有实现——后者会让测试与实现强耦合、难以重构。如果私有逻辑复杂到需要单独测,往往说明它该被抽成一个可独立测试的单元。

08.9

实战练习与资源

延伸资源