分数页面能正常显示,点“加分”后却可能算错,单靠运行 App 不一定看得出来。
最快做法:先把容易验证的业务逻辑从 SwiftUI 界面里分离,再放进测试目标,用 Swift Testing 写断言并运行;界面交互还要单独验收。

第一次给 SwiftUI 课程项目写单元测试、还不熟悉 Xcode 测试目标的学生,可以照步骤完成首个测试。
已经能运行项目、担心数据处理或状态变化出错的自学者,也可以用同一方法检查逻辑。
如果使用学校或远程 Mac 跟课,文末的环境验收与保存结果说明也适用。

01

先选一个能独立检查的逻辑

单元测试像批改一道小题:给程序一个明确输入,执行一个动作,再核对结果是否符合预期。测试目标则像作业本里专门放“检查题”的区域;断言是判分条件,负责把实际结果和预期结果作比较。

例如,SwiftUI 页面显示当前分数,画面本身正确并不能证明“加分”规则正确。若点击一次后分数应增加 1,却被写成增加 2,界面仍可能正常绘制,只是显示了错误结果。把计分规则放进独立类型后,就能不启动页面,直接检查计算结果。

Apple 的 SwiftUI 教学示例也采用了把业务逻辑放进独立类型、由视图调用的做法,便于为逻辑编写单元测试。可参考 Apple 的 Swift Testing 教学:为记分应用添加功能。

检查对象 适合验证什么 需要运行 App 界面吗 新手先做什么
单元测试 计数、输入校验、状态转换等可直接调用的逻辑 通常不需要 选一个输入和明确预期值
界面交互测试 按钮、导航、输入框等操作流程 需要 确认课程是否要求自动化操作
手动运行 App 页面布局、文字、实际交互是否符合要求 需要 用模拟器或课程要求的设备检查

因此,先挑一个输入简单、结果清楚的小功能。比如“分数不能小于 0”“每次点击加 1”,都比一上来检查整个页面更容易定位问题。

如果计算写在 View 的 body 或按钮闭包里,不必急着重写整页。可以先把与界面无关的计算挪到独立的结构体或函数,让视图只负责显示状态和传递操作。项目功能越多,越要避免把布局、数据和规则都挤在同一处,否则测试失败时很难判断问题来自哪一层。

02

准备 Xcode 27 测试目标

测试目标是 Xcode 项目里单独构建测试代码的目标;它能找到放在其中的测试文件,并按当前方案运行测试。Apple 文档说明,Xcode 16 及之后创建项目时,可以在项目选项中选择测试系统,让 Xcode 配置测试包;已有项目也可以再添加测试目标。菜单文字可能随版本或模板改变,动手前应以正在使用的 Xcode 文档和界面为准。

项目状态 检查方向 关键决策
新建项目时已选测试配置 项目导航器中是否已有单元测试目标与模板文件 有现成目标就先复用,不必重复创建
现有项目还没有测试目标 在项目设置中查看已有目标,再按官方指引添加单元测试目标 只有确认缺少目标,才创建新目标
测试文件已创建但没被发现 检查文件是否归属于单元测试目标,以及函数声明和框架导入 先修归属与声明,不要先清缓存或重装

在 Xcode 中,可按官方文档核对项目配置:新项目的测试系统选项,以及现有项目通过 File > New > Target 添加测试目标的流程。创建测试文件时,选择用于 Swift Testing 的单元测试模板,并确认文件归属正确。具体模板或界面名称若与你当前安装的版本不一致,先查看 Apple 的 Xcode 项目添加测试说明 和 配置项目新目标的文档。

⚠️ 测试文件看起来在项目里,不代表它一定属于测试目标。先检查文件的目标归属,再检查测试导航器中是否出现对应测试。

测试代码通常导入 Testing。不要为了写 Swift Testing 而把导入语句随手加进 App 页面文件;测试框架应导入测试文件中。项目目标的作用可以进一步参考 Apple 对 Xcode target 的说明。

03

写下第一条 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”后,可以添加边界情形,例如分数达到课程设定上限时是否仍能增加。边界情形不是随便塞入极端数字,而是规则可能改变、容易出错的输入边缘。若功能规定分数不得超过上限,就要在实现中明确这条规则,再按实际规则写对应测试;没有此规定时,不要把上限行为擅自当成需求。

04

运行结果与测试缺失的排查

最直接的做法是在 Xcode 中选择当前方案并运行测试;也可以在编辑器的测试函数旁,或测试导航器中找到单项测试的运行入口。Apple 提供了 运行测试与解读结果的说明,其中也介绍了通过 Product > Test 运行测试,以及查看测试报告的方法。

若项目已配置命令行构建方案,也可用 xcodebuild 运行测试:

xcodebuild test -scheme SampleApp

把 SampleApp 换成项目实际使用的方案名称。执行成功后,重点查看测试是否被识别、构建是否通过,以及测试结果是通过还是失败;命令行运行和结果查看也可对照 Apple 的运行测试文档。

看到的现象 优先核对 处理方向
测试没有出现在导航器 测试文件是否属于测试目标;是否导入 Testing;函数是否带 @Test 先修正文件归属与声明,再重新查看测试列表
测试出现,但构建失败 测试代码是否能访问被测类型;函数调用与类型声明是否一致 先解决编译信息指出的目标或代码问题
测试运行后断言失败 实际输入、预期值和业务规则是否一致 对照失败表达式,判断是测试预期写错还是逻辑有缺陷
逻辑测试通过,页面操作不对 按钮是否调用了预期逻辑;状态是否正确传回视图 单独运行 App,检查界面绑定和交互

测试缺失时,不要先把问题归咎于缓存。先确认测试文件在正确目标内、框架导入位置合理、函数使用 Swift Testing 的声明方式。若测试已经出现但失败,则分别检查测试目标执行情况、输入设置和实际逻辑;失败信息里的表达式通常能帮助缩小范围。

💡 新手常把“App 能编译”和“测试能运行”当成同一件事。前者说明应用目标构建通过,后者还要求测试目标正确配置并能发现测试函数。

05

完成课程级验收再决定环境

首个测试通过后,至少补上一个代表性的边界检查,并重新运行相关测试。随后单独启动 App,确认页面上的按钮确实触发了被测逻辑,显示状态也符合课程要求。

验收结果 能说明什么 不能据此断定什么
逻辑测试通过 被检查的输入和规则符合断言 不能证明整个界面都可用
模拟器中能运行并完成交互 当前模拟器环境下,所走的操作流程可用 不能代替课程规定的真机或其他验收
真机或课程要求的验收通过 已完成对应的设备或课程检查 不能替代之后新增逻辑的回归测试

学校要求使用模拟器或真机时,按课程标准完成对应验收;如果只是在学习业务逻辑,先掌握测试目标和断言已经能建立可重复的检查方式。需要核对特定版本能否安装时,查看 Apple 的 Xcode 系统要求:例如官方当前列出的 Xcode 27.2 beta 要求 macOS Tahoe 26.6 或更高版本。Xcode 27 不同构建版本的要求可能不同,不能把某个 beta 版本条件套用到所有安装包。

如果手边没有兼容 Mac,可先阅读没有 Mac 学习 iOS 开发的环境选择指南,再判断课程是否必须完成模拟器或真机步骤。对于只想阶段性练习 SwiftUI、需要 Xcode 项目和测试运行环境的学习者,NodeMini 的远程 Mac 可以作为临时环境选项;它不能替代课程要求的实体设备验收,也未必适合长期、高频使用固定开发环境的人。可先了解 NodeMini 的 Mac 云算力方案,再按课程要求、使用周期和连接条件决定是否租用。