本节摘要:本节是全剧最后一场:用 XCTest 给核心逻辑写单元测试(含错误分支与内存行为),用断点与内存图做工程化排障,最后用一张按界面生命周期组织的终检清单复盘全册。结论先行:测试锁行为、调试省猜测、清单防回归——三件事都是给"下一次修改"买的保险。
工作台(8.1 节)搭好,最后开两间房:彩排场(测试)与监控室(调试),再发一张巡演手册(风格与清单)。
测试类的骨架是 setUp 准备、断言验证、tearDown 收拾。给第3章的排片模型写第一组测试:
import XCTest // 测试目标来自 8.1 节的模型包(示意引用) final class SessionTests: XCTestCase { var session: Session! // 隐式解包:setUp 里必赋值(1.2 节说过的"编译器已证明"场景) override func setUp() { // 每个测试方法前都会跑一遍,彼此隔离 super.setUp() session = Session(filmName: "雾中灯塔", hall: .imax, minutes: 118, seatsLeft: 4) } func testIsAlmostFull_TrueWhenSeatsLow() { XCTAssertTrue(session.isAlmostFull, "余票 4 座应判定为告急") // 断言 + 失败说明 } func testSummary_ContainsHallName() { XCTAssertTrue(session.summary().contains("IMAX"), "摘要应包含厅名") } }
断言家族按用途选:XCTAssertTrue 验布尔、XCTAssertEqual 验相等(带精度参数验浮点)、XCTAssertNil 验可选、XCTAssertThrowsError 验抛错。错误分支必须测——2.3 节的每条失败路径都值得一个测试:
final class BoardErrorTests: XCTestCase { func testLoadBoard_ThrowsUnauthorized_WhenTokenEmpty() { XCTAssertThrowsError(try fetchBoard(token: "")) { error in guard case BoardError.unauthorized = error else { // 模式匹配验证错误类型 return XCTFail("应为未登录错误,实际 \(error)") } } } func testDecode_MapsFieldError() { // 2.3 节的 decodeFailed 分支:坏数据进来要抛带字段名的错误 XCTAssertThrowsError(try JSONDecoder().decode(SessionDTO.self, from: Data("{}".utf8))) } }
内存行为也能测——第4.4 节的泄漏实验升级成自动断言(XCTestExpectation 等待 deinit 回调,或在 addTeardownBlock 里检查弱引用已置 nil)。测试跑法:Cmd+U 跑当前 Scheme 的全部测试,报告逐条列出绿红。
⚠️ 常见坑:只测成功路径。失败分支、边界值(0 座、空数组、超长字符串)才是回归的重灾区——错误处理写了不测,等于安全气囊没装在车上。
调试三件套从轻到重:
print 大法适合临时看一眼,但断点更专业:在行号点一下出现蓝色标记,运行到该处暂停,底部调试区直接查看每个变量的当前值;po 命令(print object)能在控制台对当前上下文求任意表达式。条件断点(右键断点 → 编辑条件)适合"循环第 38 次才出错"的场景——只有条件成立才停,不用手按 37 次继续。
视图层级调试:运行时点调试栏的"视图层级"按钮,界面以三维爆炸图展开, misplaced 的约束、被遮挡的按钮一眼现形——UIKit 排版问题的定位效率提升一个数量级。
内存图:调试栏的内存图按钮打开对象关系图,按类名搜"排片页",能看到有几个实例、各自被谁引用。第4.2 节的循环引用实验在这里工程化:反复进出页面后内存图里若出现两个 BoardViewController 实例,点开它的引用链,环就画在图上。配合 Instruments(模板选 Leaks)可长时间观测泄漏曲线。
排障路径速查: 界面不动了 → 疑死锁:暂停按钮看主线程栈,是否停在 sync 派发(5.1 节事故) 对象不释放 → 内存图搜类名,看引用链成环位置(4.2 节) 界面数据不对 → 断点停在状态写入处,逐变量核对(7.1 节链路) 解析失败 → 断点在 do-catch 的 catch,po error 看详情(2.3 节)
风格约定不用从零发明:官方 API 设计准则与社区流行的格式约定(缩进、逗号尾缀、访问级别顺序)足够起步,关键是团队一致。三条对教程读者立刻有用的约定:优先 let(1.2 节)、类型当文档用(可选值与 throws 写进签名)、方法不超过一屏(超了就拆,第2.1 节的嵌套函数是拆法之一)。
最后的终检清单,按界面的一生把全册要点归位——这张表本身就是教程的复习地图:
| 生命周期时刻 | 必查项 | 出处 |
|---|---|---|
| 开演前(编译期) | 类型标注完整、let 优先、无强制解包散落 | 第1章 |
| 出生(init/加载) | 可选值建模数据未到位、错误枚举覆盖失败分支 | 第1、2章 |
| 登台(appear) | 异步任务持柄、回调 weak 守卫 | 第5、4章 |
| 演出中(交互) | 状态经单一通道修改、private(set) 收窄写权限 | 第3章 |
| 同台(并发) | 后台干活主线程刷新、QoS 已标注 | 第5章 |
| 进后台 | 轮询已停、通知已撤 | 第7章 |
| 退场(disappear) | 任务已 cancel、定时器已 invalidate | 第5、7章 |
| 谢幕(deinit) | 无循环引用、deinit 打印可观测 | 第4章 |
| 复盘(工程) | 测试覆盖错误分支、依赖走 SPM、风格一致 | 第8章 |
💡 关键直觉:测试、调试、清单的共同身份是"把知识固化成可重复执行的检查"。人的记性会衰减,清单不会——终幕交给你的不是三条技巧,而是对抗遗忘的机制。
从序幕的第一句 print 到终幕的测试报告,排片界面的一生演完了。剧场的灯亮起,观众散场,而你已知道这座剧场每一块地板下藏着什么——下一次大幕拉开,导演是你。