一个宏实现合并后,本地测试全部通过,云端 Mac 的下一次构建却生成了不同的成员顺序,诊断位置也偏了一行。最终产物仍能编译,但代码审查看到的已不是同一套 API。Swift 宏运行在编译阶段,结果同时受宏实现、SwiftSyntax 依赖和当前工具链影响,因此不能只把它当普通函数测。
先定义要锁定的结果
回归测试应覆盖三层。第一层比较宏展开后的完整源码,捕捉成员、访问级别、属性和格式变化。第二层验证错误、警告与修复建议,尤其是诊断所指向的 token。第三层才是编译和运行测试,用来确认生成代码可被真实调用。
| 层级 | 检查对象 | 典型失败 |
|---|---|---|
| 展开层 | 生成源码与声明顺序 | 属性遗漏、访问级别改变 |
| 诊断层 | 消息、位置、修复建议 | 错误落在错误节点 |
| 行为层 | 编译与运行结果 | 生成实现可编译但行为错误 |
不要把“测试能编译”当成“宏输出没有变化”。宏的公开契约通常就是它生成的源码形状。
先挑选最小输入建立基线,再补空声明、泛型、嵌套类型、已有同名成员和非法参数。每个用例只验证一种规则,失败时才能快速判断是实现回归还是工具链变化。
用展开测试固定生成源码
宏包的测试目标可引入 SwiftSyntaxMacrosTestSupport,通过 assertMacroExpansion 同时给出输入和期望源码。下面的用例假设 TraceIDMacro 为结构体补充一个字符串属性:
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import TraceIDMacros
private let macros: [String: Macro.Type] = [
"TraceID": TraceIDMacro.self
]
final class TraceIDMacroTests: XCTestCase {
func testAddsTraceID() {
assertMacroExpansion(
"""
@TraceID
struct Job {}
""",
expandedSource:
"""
struct Job {
let traceID: String
}
""",
macros: macros
)
}
}
期望文本应保持完整,不要只搜索 traceID 是否出现。局部字符串断言会漏掉重复成员、错误缩进和意外增加的接口。若宏允许参数,还应为默认值、显式值和非法表达式分别建例。
把错误路径单独拆开
非法输入不要和成功展开混在同一个测试中。分别断言诊断文本、行列位置及修复建议,并给测试名写清触发条件。这样调整一条错误信息时,不会迫使团队重新接受整份成功快照。
记录工具链指纹
BookaMac 上的云端 Mac 可以长期执行同一条流水线,但稳定节点不等于工具链永远不变。每次测试都应把 Xcode 选择路径、Swift 版本、主机架构和依赖锁文件一起归档。
set -euo pipefail
export LANG=C
export LC_ALL=C
mkdir -p artifacts
xcode-select -p | tee artifacts/xcode-path.txt
xcrun swift --version | tee artifacts/swift-version.txt
uname -m | tee artifacts/host-arch.txt
cp Package.resolved artifacts/Package.resolved
xcrun swift test 2>&1 | tee artifacts/swift-test.log
LANG 与 LC_ALL 固定后,诊断文本不易因运行账户的语言环境发生变化。xcrun 则确保测试使用当前选中的 Xcode 工具链,而不是 shell 路径里偶然出现的另一个 swift。如果仓库没有 Package.resolved,脚本应明确跳过复制,而不是创建空文件冒充锁定结果。
隔离并发与共享状态
宏实现若读取环境变量、当前目录、时间或临时文件,测试很容易在并行执行时漂移。更稳妥的原则是让宏只依据语法树和显式参数生成结果。确实需要外部数据时,把读取逻辑抽到独立类型,并在测试中注入固定值。
先以串行模式跑通最小集合,再开启并行测试。若并行后才失败,优先检查固定文件名、共享缓存目录、全局可变变量和测试清理顺序,不要直接增加重试次数。重试会把竞态隐藏成偶发绿灯。
宏测试也不应修改真实工程源码。临时输入写入每个用例独立的目录,完成后清理;失败时只保留脱敏后的输入、展开结果和日志。这样既能复现,也不会让上一轮残留影响下一轮。
安全升级工具链
升级 Xcode 或 SwiftSyntax 前,先在旧工具链运行全套测试并保存基线,再切换工具链执行同一命令。审查差异时按以下顺序处理:
- 确认实际选中的 Xcode 路径与 Swift 版本。
- 检查
Package.resolved是否发生预期内变化。 - 区分空白、缩进等格式变化与成员、类型、访问级别等语义变化。
- 重新运行行为测试,确认生成接口可被调用。
- 只有完成审查后,才更新展开期望值。
如果所有用例同时出现相似格式差异,通常应先调查打印规则或工具链变化;如果只有一个边界用例变化,则更可能是宏实现自身的问题。不要用批量替换直接刷新快照,否则真正的 API 回归会混在大量文本变化里。
最终应保留三类产物:机器可读的测试结果、展开失败的实际与期望差异、工具链指纹。下一次云端 Mac 构建失败时,排查起点就不再是“环境可能不一样”,而是可以明确回答哪一层契约、从哪个版本开始发生了变化。
常见问题
Swift 宏测试是否只需要比较最终编译结果?
不够。最终编译通过只能证明生成代码可编译,还应直接比较展开源码,并单独验证错误、警告和修复建议,才能发现 API 形状或诊断位置的意外变化。
工具链升级后所有展开快照都变化怎么办?
先记录升级前后的 Xcode 路径、Swift 版本和依赖锁文件,再区分纯格式变化与语义变化。只有确认公开 API、诊断和运行行为均符合预期后,才批量更新基线。
把已验证的工作流放到持续在线的云端 Mac
按用途选择 BookaMac M4 或 BookaMac M4 Pro,并在下单时核对地区、租期与存储附加项。