分数页面能正常显示,点“加分”后却可能算错,单靠运行 App 不一定看得出来。
最快做法:先把容易验证的业务逻辑从 SwiftUI 界面里分离,再放进测试目标,用 Swift Testing 写断言并运行;界面交互还要单独验收。
第一次给 SwiftUI 课程项目写单元测试、还不熟悉 Xcode 测试目标的学生,可以照步骤完成首个测试。
已经能运行项目、担心数据处理或状态变化出错的自学者,也可以用同一方法检查逻辑。
如果使用学校或远程 Mac 跟课,文末的环境验收与保存结果说明也适用。
先选一个能独立检查的逻辑
单元测试像批改一道小题:给程序一个明确输入,执行一个动作,再核对结果是否符合预期。测试目标则像作业本里专门放“检查题”的区域;断言是判分条件,负责把实际结果和预期结果作比较。
例如,SwiftUI 页面显示当前分数,画面本身正确并不能证明“加分”规则正确。若点击一次后分数应增加 1,却被写成增加 2,界面仍可能正常绘制,只是显示了错误结果。把计分规则放进独立类型后,就能不启动页面,直接检查计算结果。
Apple 的 SwiftUI 教学示例也采用了把业务逻辑放进独立类型、由视图调用的做法,便于为逻辑编写单元测试。可参考 Apple 的 Swift Testing 教学:为记分应用添加功能。
| 检查对象 | 适合验证什么 | 需要运行 App 界面吗 | 新手先做什么 |
|---|---|---|---|
| 单元测试 | 计数、输入校验、状态转换等可直接调用的逻辑 | 通常不需要 | 选一个输入和明确预期值 |
| 界面交互测试 | 按钮、导航、输入框等操作流程 | 需要 | 确认课程是否要求自动化操作 |
| 手动运行 App | 页面布局、文字、实际交互是否符合要求 | 需要 | 用模拟器或课程要求的设备检查 |
因此,先挑一个输入简单、结果清楚的小功能。比如“分数不能小于 0”“每次点击加 1”,都比一上来检查整个页面更容易定位问题。
如果计算写在 View 的 body 或按钮闭包里,不必急着重写整页。可以先把与界面无关的计算挪到独立的结构体或函数,让视图只负责显示状态和传递操作。项目功能越多,越要避免把布局、数据和规则都挤在同一处,否则测试失败时很难判断问题来自哪一层。
准备 Xcode 27 测试目标
测试目标是 Xcode 项目里单独构建测试代码的目标;它能找到放在其中的测试文件,并按当前方案运行测试。Apple 文档说明,Xcode 16 及之后创建项目时,可以在项目选项中选择测试系统,让 Xcode 配置测试包;已有项目也可以再添加测试目标。菜单文字可能随版本或模板改变,动手前应以正在使用的 Xcode 文档和界面为准。
| 项目状态 | 检查方向 | 关键决策 |
|---|---|---|
| 新建项目时已选测试配置 | 项目导航器中是否已有单元测试目标与模板文件 | 有现成目标就先复用,不必重复创建 |
| 现有项目还没有测试目标 | 在项目设置中查看已有目标,再按官方指引添加单元测试目标 | 只有确认缺少目标,才创建新目标 |
| 测试文件已创建但没被发现 | 检查文件是否归属于单元测试目标,以及函数声明和框架导入 | 先修归属与声明,不要先清缓存或重装 |
在 Xcode 中,可按官方文档核对项目配置:新项目的测试系统选项,以及现有项目通过 File > New > Target 添加测试目标的流程。创建测试文件时,选择用于 Swift Testing 的单元测试模板,并确认文件归属正确。具体模板或界面名称若与你当前安装的版本不一致,先查看 Apple 的 Xcode 项目添加测试说明 和 配置项目新目标的文档。
⚠️ 测试文件看起来在项目里,不代表它一定属于测试目标。先检查文件的目标归属,再检查测试导航器中是否出现对应测试。
测试代码通常导入 Testing。不要为了写 Swift Testing 而把导入语句随手加进 App 页面文件;测试框架应导入测试文件中。项目目标的作用可以进一步参考 Apple 对 Xcode target 的说明。
写下第一条 Swift Testing 测试
下面用分数计数演示最小的“准备输入—执行操作—检查结果”。ScoreCounter 只保存分数并提供加分操作,不依赖 SwiftUI 页面;测试则确认调用一次后分数符合预期。
struct ScoreCounter {
private(set) var score = 0
mutating func addPoint() {
score += 1
}
}
在单元测试目标中的 Swift 文件里写:
import Testing
@Test func addingPointIncreasesScoreByOne() {
var counter = ScoreCounter()
counter.addPoint()
#expect(counter.score == 1)
}
这段测试依次完成三件事:创建初始分数为 0 的对象;调用一次 addPoint();用 #expect 核对结果是否为 1。@Test 标记测试函数,#expect 则检查表达式是否符合预期。若条件不成立,测试会失败,并提供失败信息。可以查看 Apple 关于 Swift Testing 预期条件的文档 和 Swift Testing 框架介绍。
这里的示例假定 ScoreCounter 位于同一测试目标可访问的 App 模块中;若实际项目把类型放在另一个模块,要按项目配置确认测试目标能访问该代码。不要仅因为示例能粘贴,就跳过构建检查。
一个测试只检查一个清楚的规则。完成“加 1”后,可以添加边界情形,例如分数达到课程设定上限时是否仍能增加。边界情形不是随便塞入极端数字,而是规则可能改变、容易出错的输入边缘。若功能规定分数不得超过上限,就要在实现中明确这条规则,再按实际规则写对应测试;没有此规定时,不要把上限行为擅自当成需求。
运行结果与测试缺失的排查
最直接的做法是在 Xcode 中选择当前方案并运行测试;也可以在编辑器的测试函数旁,或测试导航器中找到单项测试的运行入口。Apple 提供了 运行测试与解读结果的说明,其中也介绍了通过 Product > Test 运行测试,以及查看测试报告的方法。
若项目已配置命令行构建方案,也可用 xcodebuild 运行测试:
xcodebuild test -scheme SampleApp
把 SampleApp 换成项目实际使用的方案名称。执行成功后,重点查看测试是否被识别、构建是否通过,以及测试结果是通过还是失败;命令行运行和结果查看也可对照 Apple 的运行测试文档。
| 看到的现象 | 优先核对 | 处理方向 |
|---|---|---|
| 测试没有出现在导航器 | 测试文件是否属于测试目标;是否导入 Testing;函数是否带 @Test |
先修正文件归属与声明,再重新查看测试列表 |
| 测试出现,但构建失败 | 测试代码是否能访问被测类型;函数调用与类型声明是否一致 | 先解决编译信息指出的目标或代码问题 |
| 测试运行后断言失败 | 实际输入、预期值和业务规则是否一致 | 对照失败表达式,判断是测试预期写错还是逻辑有缺陷 |
| 逻辑测试通过,页面操作不对 | 按钮是否调用了预期逻辑;状态是否正确传回视图 | 单独运行 App,检查界面绑定和交互 |
测试缺失时,不要先把问题归咎于缓存。先确认测试文件在正确目标内、框架导入位置合理、函数使用 Swift Testing 的声明方式。若测试已经出现但失败,则分别检查测试目标执行情况、输入设置和实际逻辑;失败信息里的表达式通常能帮助缩小范围。
💡 新手常把“App 能编译”和“测试能运行”当成同一件事。前者说明应用目标构建通过,后者还要求测试目标正确配置并能发现测试函数。
完成课程级验收再决定环境
首个测试通过后,至少补上一个代表性的边界检查,并重新运行相关测试。随后单独启动 App,确认页面上的按钮确实触发了被测逻辑,显示状态也符合课程要求。
| 验收结果 | 能说明什么 | 不能据此断定什么 |
|---|---|---|
| 逻辑测试通过 | 被检查的输入和规则符合断言 | 不能证明整个界面都可用 |
| 模拟器中能运行并完成交互 | 当前模拟器环境下,所走的操作流程可用 | 不能代替课程规定的真机或其他验收 |
| 真机或课程要求的验收通过 | 已完成对应的设备或课程检查 | 不能替代之后新增逻辑的回归测试 |
学校要求使用模拟器或真机时,按课程标准完成对应验收;如果只是在学习业务逻辑,先掌握测试目标和断言已经能建立可重复的检查方式。需要核对特定版本能否安装时,查看 Apple 的 Xcode 系统要求:例如官方当前列出的 Xcode 27.2 beta 要求 macOS Tahoe 26.6 或更高版本。Xcode 27 不同构建版本的要求可能不同,不能把某个 beta 版本条件套用到所有安装包。
如果手边没有兼容 Mac,可先阅读没有 Mac 学习 iOS 开发的环境选择指南,再判断课程是否必须完成模拟器或真机步骤。对于只想阶段性练习 SwiftUI、需要 Xcode 项目和测试运行环境的学习者,NodeMini 的远程 Mac 可以作为临时环境选项;它不能替代课程要求的实体设备验收,也未必适合长期、高频使用固定开发环境的人。可先了解 NodeMini 的 Mac 云算力方案,再按课程要求、使用周期和连接条件决定是否租用。