- 代码生成
- 开发工具
【免费下载链接】Sourcery
Meta-programming for Swift, stop writing boilerplate code.
Sourcery 是 Swift 的元编程工具,用于自动生成样板代码。本文围绕仓库 guides/Diffable.md 中讲解的 Diffable 模板展开,说明如何让测试断言失败时的输出从"一整面墙的文本"变成"精确到属性级别的差异报告"。读完本文,你将掌握 Diffable 模板的作用原理、AutoDiffable协议的使用方式、skipEquality注解的用法,以及差异结果的输出格式与底层实现。
Diffable 模板解决的问题很具体:在测试中做相等性比较时,一旦失败,默认的断言输出往往是整块对象的文本描述,难以快速定位到底是哪个属性不一致。Diffable 模板基于 SourceryRuntime/Sources/Common/Diffable.swift 中提供的Diffable协议与DiffableResult容器,为遵循AutoDiffable协议的类型自动生成diffAgainst(_:)方法,从而输出按属性逐项对比的精确差异。
一、模板要解决的痛点
在测试中对比两个对象是否相等是常见操作,但默认输出对排查问题并不友好。例如一个包含多个属性的模型对象,断言失败时只会输出整个对象的一整段文本,开发者需要在一大段文字里人工比对属性值。
Diffable 模板的目标就是把这种体验从"读一整面墙的文本"(before)改造成"精确到属性级别的差异"(after):失败信息会逐条列出哪个属性、期望值是什么、实际值是什么,甚至能定位到数组中的下标或字典中的键。
二、Diffable 模板与 AutoDiffable 协议
模板的完整实现位于 Sourcery/Templates/Diffable.stencil,它遍历所有实现AutoDiffable协议的类型,为每个类型生成一个diffAgainst(_ object: Any?) -> DiffableResult扩展方法。
模板核心逻辑如下:
{% for type in types.implementing.AutoDiffable|!protocol|!annotated:"skipDiffing" %} extension {{ type.name }}: Diffable { public func diffAgainst(_ object: Any?) -> DiffableResult { let results = DiffableResult() guard let castObject = object as? {{ type.name }} else { results.append("Incorrect type <expected: {{ type.name }}, received: \(Swift.type(of: object))>") return results } {% for variable in type.storedVariables|!annotated:"skipEquality" %} results.append(contentsOf: DiffableResult(identifier: "{{ variable.name }}") .trackDifference(actual: self.{{ variable.name }}, expected: castObject.{{ variable.name }})) {% endfor %} return results } } {% endfor %}生成逻辑可以拆解为四步:
- 筛选类型:从
types.implementing.AutoDiffable中取出所有实现AutoDiffable协议的类型,并通过!protocol排除协议本身、通过!annotated:"skipDiffing"排除打了skipDiffing注解的类型。 - 类型校验:先把待比较对象强转为目标类型,类型不匹配时直接记录
Incorrect type <expected: 类型名, received: 实际类型>并返回,保证后续属性访问安全。 - 逐属性对比:遍历该类型的所有
storedVariables(存储属性),排除带skipEquality注解的属性,对每个属性用DiffableResult(identifier: 属性名)包裹,再调用trackDifference(actual:expected:)记录差异。 - 汇总返回:把每个属性的差异结果拼接进总的
DiffableResult返回给调用方。
其中AutoDiffable是一个幽灵协议(phantom protocol),只起标记作用,实现位于 SourceryRuntime/Sources/Common/AST/PhantomProtocols.swift:
protocol AutoDiffable {}它不声明任何方法,纯粹作为模板筛选的"标签"。只要让你的类型遵守它,模板就会为该类型生成Diffable实现。仓库自身的运行时类型也大量使用这一约定,例如 Modifier.swift 和 Attribute.swift 都同时声明了AutoDiffable与Diffable。从源码结构看,运行时自身的模型也通过同一个模板机制生成差异代码,只是生成产物已预编译进框架。
三、可用的注解
模板暴露了两个注解来控制生成行为:
skipEquality
skipEquality用于跳过某个属性的比较。模板在遍历storedVariables时用!annotated:"skipEquality"将其过滤掉,注解直接写在属性声明的上方注释里:
struct User: AutoDiffable { let id: Int // sourcery: skipEquality let lastLoginDate: Date? }生成后的diffAgainst只比较id,lastLoginDate被排除在外,适合忽略时间戳、缓存、内部状态等不稳定属性。该注解的解析逻辑由 SourceryFramework/Sources/Utils/AnnotationsParser.swift 负责,相关解析用例可参见 SourceryTests/Parsing/Helpers/AnnotationsParserSpec.swift。
skipDiffing
skipDiffing用于跳过整个类型的生成。模板在第一行的类型筛选中通过!annotated:"skipDiffing"排除带此注解的类型,适合不想让某个遵守AutoDiffable的类型生成代码的场景(例如由其他模板接管)。
四、DiffableResult 差异容器与输出格式
生成的方法返回DiffableResult,这是 SourceryRuntime/Sources/Common/Diffable.swift 中定义的一个容器类,负责收集并格式化差异信息。
核心输出格式由其description决定(Diffable.swift):
public override var description: String { guard !results.isEmpty else { return "" } var description = "\(identifier.flatMap { "\($0) " } ?? "")" description.append(results.joined(separator: "\n")) return description }即:先输出identifier(属性名)作为前缀,再把该属性下收集到的所有差异条目用换行符连接。聚合后的整体输出形如:
localName <expected: Bar, received: Foo>其中localName是属性标识,<expected: Bar, received: Foo>是差异条目。整个DiffableResult遵守AutoEquatable且自带hash、isEqual实现,并声明了skipEquality、skipJSExport注解,说明它自身同样由 Sourcery 的自动代码生成体系维护(Diffable.swift)。
trackDifference系列方法是差异对比的核心,针对不同数据结构有不同重载(Diffable.swift):
- 标量值:直接比较
Equatable值,不等时记录<expected: 期望值, received: 实际值>; - 可空值:把
nil显示为字面量nil,再按标量规则比较; - 嵌套 Diffable 对象:调用对象的
diffAgainst递归展开,让差异深入对象内部; - 数组:先比较数量,数量一致时逐下标比较,差异条目带
idx N:前缀,便于定位元素位置; - 字典:先比较数量,再逐键比较,缺失的键会被汇总为
Missing keys:列表,差异条目带key "K":前缀; - NSObject 字典:针对
NSObjectProtocol值提供基于isEqual的字典比较重载。
这些重载的行为都有对应的单元测试覆盖,位于 SourceryTests/Models/DiffableSpec.swift,测试断言了以下典型输出:
- 标量不等:
<expected: 5, received: 3>; - 嵌套对象属性不等:
localName <expected: Bar, received: Foo>; - 数组数量不等:
Different count, expected: 2, received: 1; - 数组元素不等:
idx 1: localName <expected: Foo2, received: Foo>; - 字典键缺失:
Different count, expected: 2, received: 1\nMissing keys: Something; - 字典值不等:
key "Something": localName <expected: Bar, received: FooBar>。
五、在测试中的接入方式
接入流程分三步:
让模型遵守
AutoDiffable:在源码类型声明处加上协议,例如struct User: AutoDiffable,并按需用skipEquality注解排除不稳定属性。运行 Sourcery 生成代码:把
Sourcery/Templates/Diffable.stencil加入模板路径,通过命令行或.sourcery.yml配置生成到测试目标。命令行为例:./bin/sourcery --sources <源码目录> --templates <Diffable.stencil 所在目录> --output <生成目录>也可以改用
.sourcery.yml配置文件指定sources、templates、output三组路径,详见 README.md。在测试断言中使用
diffAgainst:断言失败时调用actual.diffAgainst(expected),把返回的DiffableResult的description写入失败信息,即可得到属性级别的精确差异。
生成的代码是普通扩展,可以放进测试 target 的生成目录,不影响运行时行为。由于模板只遍历storedVariables,计算属性不会参与对比;同时比较的是"实际值 vs 期望值"(actual在前、expected在后),阅读输出时注意方向。
六、小结
Diffable 模板把测试中的对象对比从"整块文本墙"升级为"逐属性差异报告":以AutoDiffable幽灵协议作为生成开关,用skipEquality和skipDiffing两个注解控制粒度,由DiffableResult统一收集格式化差异,并对标量、可空值、嵌套对象、数组、字典提供了递归对比能力。模板文件在 Sourcery/Templates/Diffable.stencil,运行时支持在 SourceryRuntime/Sources/Common/Diffable.swift,行为验证在 SourceryTests/Models/DiffableSpec.swift,这三处共同构成了从模板到运行时的完整链路。
- 代码生成
- 开发工具
【免费下载链接】Sourcery
Meta-programming for Swift, stop writing boilerplate code.
相关推荐
Android-Next核心组件完全解析:高效Android开发的秘密武器
Android Next核心组件完全解析:高效Android开发的秘密武器 Android Next是一套功能强大的Android公共组件库,专为提升开发效率和
开发工具Java-AES-Crypto密码学基础:避免Android加密的7个常见错误
Java AES Crypto密码学基础:避免Android加密的7个常见错误 在Android应用开发中,数据安全是至关重要的环节。Java AES Cryp
Sourcery自动化测试策略:确保生成代码的可靠性
Sourcery自动化测试策略:确保生成代码的可靠性 Meta Programming(元编程)技术能显著减少重复性代码工作,但生成代码的可靠性验证一直是开发中
代码生成开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考