☰
用 Sourcery 的 Diffable 模板生成精确到属性级别的测试差异输出
2026/9/26 8:05:36 网站建设 项目流程
  • 代码生成
  • 开发工具

【免费下载链接】Sourcery

Meta-programming for Swift, stop writing boilerplate code.

项目地址:https://gitcode.com/gh_mirrors/so/Sourcery
点击查看免费下载

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 %}

生成逻辑可以拆解为四步:

  1. 筛选类型:从types.implementing.AutoDiffable中取出所有实现AutoDiffable协议的类型,并通过!protocol排除协议本身、通过!annotated:"skipDiffing"排除打了skipDiffing注解的类型。
  2. 类型校验:先把待比较对象强转为目标类型,类型不匹配时直接记录Incorrect type <expected: 类型名, received: 实际类型>并返回,保证后续属性访问安全。
  3. 逐属性对比:遍历该类型的所有storedVariables(存储属性),排除带skipEquality注解的属性,对每个属性用DiffableResult(identifier: 属性名)包裹,再调用trackDifference(actual:expected:)记录差异。
  4. 汇总返回:把每个属性的差异结果拼接进总的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>。

五、在测试中的接入方式

接入流程分三步:

  1. 让模型遵守AutoDiffable:在源码类型声明处加上协议,例如struct User: AutoDiffable,并按需用skipEquality注解排除不稳定属性。

  2. 运行 Sourcery 生成代码:把Sourcery/Templates/Diffable.stencil加入模板路径,通过命令行或.sourcery.yml配置生成到测试目标。命令行为例:

    ./bin/sourcery --sources <源码目录> --templates <Diffable.stencil 所在目录> --output <生成目录>

    也可以改用.sourcery.yml配置文件指定sources、templates、output三组路径,详见 README.md。

  3. 在测试断言中使用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.

项目地址:https://gitcode.com/gh_mirrors/so/Sourcery
点击查看免费下载
上一篇:GHelper:华硕笔记本终极轻量控制工具,告别Armoury Crate臃肿体验
下一篇:gitness 许可证头批量回填:insert-license-headers.sh 用法与实现原理深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询