ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Sourcery AutoMockable 模板实战:为 Swift 协议自动生成测试 Mock

Sourcery AutoMockable 模板实战:为 Swift 协议自动生成测试 Mock 代码生成开发工具【免费下载链接】SourceryMeta-programming for Swift, stop writing boilerplate code.项目地址https://gitcode.com/gh_mirrors/so/Sourcery点击查看免费下载Sourcery 是 Swift 的元编程Meta-programming工具其中AutoMockable模板是其最受欢迎的玩法之一只要给协议打上一个标记就能自动生成对应的ProtocolNameMock测试桩类替你把“实现协议 记录调用 控制返回值”这套最枯燥的样板代码全部写掉。本文以 guides/Mocks.md 为主线结合 Templates/Templates/AutoMockable.stencil 模板源码与 Templates/Tests 下的测试用例讲清标记方式、生成规则、进阶能力与已知限制读完你可以在自己的测试工程里直接落地这套 Mock 生成方案。一、为什么需要自动生成协议 Mock在单元测试中我们经常需要替依赖的协议造一个“替身”既能记录这个协议被调用过几次、传入了什么参数又能按测试需要返回预设结果。手写这样的 Mock 类极其机械——每个函数都要写called标记、参数记录、返回值存放每加一个协议成员就要同步维护一遍还容易漏写。Sourcery 的AutoMockable模板正是为此设计的。它扫描源代码中的协议凡是实现了AutoMockable协议或带有AutoMockable注解的协议都会自动生成一个名为ProtocolNameMock的类类中为每个函数和属性生成对应的记录与桩逻辑。原文档 guides/Mocks.md 将其定位为能覆盖你 90% 的 Mock 需求帮你删掉最无聊的样板代码。二、两种标记方式继承协议或使用注解方式一让目标协议继承AutoMockable在测试源码中定义空协议AutoMockable凡是继承它的协议都会被模板识别见 Templates/Tests/Context/AutoMockable.swift 中的定义public protocol AutoMockable {} protocol BasicProtocol: AutoMockable { func loadConfiguration() - String? func save(configuration: String) }方式二使用sourcery: AutoMockable注解不想让被测协议“沾染”测试协议的继承关系时可以用注解标记无需改动协议声明本身见 Templates/Tests/Context/AutoMockable.swift/// sourcery: AutoMockable protocol AnnotatedProtocol { func sayHelloWith(name: String) }两种方式的底层判定在模板源码 Templates/Templates/AutoMockable.stencil 中完全等价{% for type in types.protocols where type.based.AutoMockable or type|annotated:AutoMockable %}即要么协议based于AutoMockable继承链上出现它要么被annotated注解。注解的具体书写规则单行、行尾、sourcery:begin/end区块等可参考 Writing templates 一文模板也支持type|annotated:AutoMockable这类 Stencil 过滤器。三、生成的 Mock 结构总览对每个命中条件的协议模板生成class ProtocolNameMock: ProtocolName类内按协议声明顺序输出对每个函数原样实现该函数生成名称CallsCount计数变量与Called布尔计算属性用于断言“函数是否被调用过”生成名称ReceivedArguments元组多个参数时或名称Received参数单个参数时记录最后一次调用传入的参数生成名称ReceivedInvocations数组记录历次调用的参数调用历史对有返回值的函数生成名称ReturnValue变量未设置 Closure 时函数直接返回它生成名称Closure可选闭包设置后函数会调用它并返回其结果——这是实现“按测试定制行为”的钩子。对每个属性生成同名同类型的可读写gettable and settable变量具体形式取决于属性类型可选型、数组/字典、普通非可选型各有不同见下文。以文档 guides/Mocks.md 中的示例输出为参照一个带回调的函数会生成class MockableServiceMock: MockableService { //MARK: - functionWithArguments var functionWithArgumentsCalled false var functionWithArgumentsReceivedArguments: (firstArgument: String, onComplete: (String)- Void)? //MARK: - functionWithCallback var functionWithCallbackCalled false var functionWithCallbackReceivedArguments: (firstArgument: String, onComplete: (String)- Void)? func functionWithCallback(_ firstArgument: String, onComplete: escaping (String)- Void) { functionWithCallbackCalled true functionWithCallbackReceivedArguments (firstArgument: firstArgument, onComplete: onComplete) } ... }需要注意实际模板的命名规则比文档示例更精细。为了区分重载与同名方法模板通过cleanString宏AutoMockable.stencil#L19把方法名中的( ) : - .等字符替换为下划线再用swiftifyMethodName宏AutoMockable.stencil#L20-L24拼接“方法名 返回类型名”所以真实生成的变量名形如loadConfigurationStringCallsCount、saveConfigurationStringVoidReceivedConfiguration可在 Templates/Tests/Expected/AutoMockable.expected 中直接查看。四、函数 Mock 的生成细节与源码依据函数生成逻辑集中在mockMethod宏Templates/Templates/AutoMockable.stencil#L71-L139。对照 Templates/Tests/Expected/AutoMockable.expected 中BasicProtocolMock的真实输出可以把规则拆成几块1. 调用计数与“是否被调用”var loadConfigurationStringCallsCount 0 var loadConfigurationStringCalled: Bool { return loadConfigurationStringCallsCount 0 }Called是计算属性等价于“计数大于 0”函数体第一行执行CallsCount 1。模板中对单参数函数与多参数函数的记录形式不同单参数生成Received参数名如ReceivedConfiguration: (String)?与对应的ReceivedInvocations数组多参数生成ReceivedArguments具名元组与ReceivedInvocations数组如save的(configuration: String)?。2. 返回值ReturnValue与Closure双通道对有返回值的方法模板生成ReturnValue变量并在方法体内实现“二选一”逻辑AutoMockable.stencil#L128-L133if let loadConfigurationStringClosure loadConfigurationStringClosure { return loadConfigurationStringClosure() } else { return loadConfigurationStringReturnValue }也就是说默认返回ReturnValue未赋值时非可选类型为隐式解包可选初值即崩溃——这是有意设计强迫你在测试中先设置返回值或 Closure一旦设置了Closure则转而调用 Closure。Closure类型与函数签名完全一致例如var loadConfigurationStringClosure: (() - String?)?。这给了测试两种用法// 用法一直接设置返回值 mock.loadConfigurationStringReturnValue cached // 用法二用 Closure 模拟更复杂的行为 mock.loadConfigurationStringClosure { computed-\(Int.random(in: 0...9)) }3. throws 方法与类型化错误模板对throws方法额外生成ThrowableError变量AutoMockable.stencil#L30-L32方法体先抛错再走返回逻辑见 AutoMockable.expected#L1953-L1970var doOrThrowStringThrowableError: (any Error)? var doOrThrowStringCallsCount 0 ... func doOrThrow() throws - String { doOrThrowStringCallsCount 1 if let error doOrThrowStringThrowableError { throw error } if let doOrThrowStringClosure doOrThrowStringClosure { return try doOrThrowStringClosure() } else { return doOrThrowStringReturnValue } }对于 Swift 的类型化抛出throws(CustomError)、throws(any Error)、throws(Never)模板同样生成对应的ThrowableError: (CustomError)?与Closure: (() throws(CustomError) - String)?见 TypedThrowableProtocolMock。但泛型类型化抛出generic typed throws目前未完整支持模板会生成fatalError(Generic typed throws are not fully supported yet)占位。4. async 方法模板完整支持async/async throws方法Closure携带async标记调用处使用await/try await见 AsyncProtocolMock。available等函数属性也会被原样搬到生成的实现上。5. 初始化器协议中的init会被生成为required init同样记录参数并调用对应的Closure见 InitializationProtocolMockrequired init(intParameter: Int, stringParameter: String, optionalParameter: String?) { init...ReceivedArguments (intParameter: intParameter, stringParameter: stringParameter, optionalParameter: optionalParameter) init...ReceivedInvocations.append((intParameter: intParameter, ...)) init...Closure?(intParameter, stringParameter, optionalParameter) }6. 静态方法附带static func reset()当协议含静态方法时模板额外生成static func reset()将计数、接收参数、Closure、ThrowableError 全部复位AutoMockable.stencil#L149-L169方便测试之间清理状态见 StaticMethodProtocolMock。五、属性 Mock 的生成细节属性按类型分三种策略对应模板 AutoMockable.stencil#L171-L186见 VariablesProtocolMock可选型属性——直接生成同名可选变量var company: String?数组/字典属性——生成默认空容器var kids: [String] [] var universityMarks: [String: Int] [:]普通非可选属性——生成计算属性 underlying名称隐式解包可选存储var name: String { get { return underlyingName } set(value) { underlyingName value } } var underlyingName: (String)!底层采用underlying隐式解包可选的用意与返回值的!一致提醒测试人员先给属性赋值再读取。async / throws 属性同样被支持get async、get throws、get async throws模板为其生成CallsCount/Called/Closure/ThrowableError与underlying存储见 AsyncThrowingVariablesProtocolMock。六、更进阶的生成能力关联类型associatedtype与泛型约束模板会把协议的关联类型提取为 Mock 类的泛型参数并把where约束原样搬过去见 TestProtocolMock协议关联类型若指向具体类型则生成typealias。Sendable若协议继承Sendable生成的 Mock 类会追加, unchecked Sendable以满足并发检查AutoMockable.stencil#L392-L394示例见 SendableProtocolMock。访问级别public协议生成的 Mock 类、属性和方法均带public并自动生成public init() {}见 AccessLevelProtocolMock。扩展中的默认实现模板使用!definedInExtension过滤跳过仅在扩展中定义的成员带默认实现的扩展声明无需再 mock协议本体同时声明的成员仍会生成。保留字与多行声明方法名为保留字如continue或参数跨多行声明时模板均可正确生成ReservedWordsProtocolMock。七、在项目中运行 AutoMockable命令行方式Sourcery 是命令行工具guides/Usage.md可以手动运行也可以放进 Xcode 的 custom build phase$ ./sourcery --sources sources path --templates templates path --output output path把--templates指向包含 AutoMockable.stencil 的目录即可。模板默认只import Foundation如果你的协议依赖了其他模块可以用--args传入autoMockableImports与autoMockableTestableImports模板在 AutoMockable.stencil#L11-L17 读取它们分别生成普通import与testable import$ ./sourcery --sources Sources --templates Templates/AutoMockable.stencil \ --output Generated --args autoMockableTestableImportsMyApp配置文件方式更推荐在.sourcery.yml中配置部分功能如 exclude 仅配置文件可用并搭配--watch实现模板与源码变更后的自动重新生成sources: - Sources templates: - Templates/AutoMockable.stencil output: Generated args: autoMockableTestableImports: MyApp生成结果默认写入AutoMockable.generated.swift模板名 .generated.swift。若你的模板目录还包含AutoMockable以外的模板可用配置文件的exclude排除不想生成的部分模板。八、模板如何被验证仓库测试体系仓库用一套“输入-期望输出”对比机制持续验证该模板Templates/Tests/Context/AutoMockable.swift 是精心构造的输入——覆盖了基本方法、隐式解包返回值、初始化器、只读/读写属性、重载、保留字、throws/typed throws、async、闭包参数escaping/non-escaping/可空/多参数、any/some存在类型、关联类型、Sendable、下标等 40 余种场景Templates/Tests/Expected/AutoMockable.expected 是对应的完整期望输出2187 行TemplatesTests.swift 在测试启动时用真实sourcery二进制生成文件再与期望输出逐行 diff。因此你在 Templates/Tests/Expected/AutoMockable.expected 里看到的每一种写法都是模板在当前版本下可复现的真实产物。九、已知限制与注意事项原文档 guides/Mocks.md 明确列出以下限制结合模板源码可以进一步确认重载方法会产生编译错误重载函数上方生成的记录变量名称相同如SameShortMethodNamesProtocol中两个start会生成同名记录变量模板虽然会按参数与返回类型区分命名以尽量规避但同名重载场景仍需留意。Workaround删掉其中一个函数顶部多余的记录变量或手动重命名。回调的成败分支需要自己处理对带 completion 回调的方法自动生成只能记录并转发回调无法推断“成功时调用回调 A、失败时调用回调 B”的业务语义——这部分必须利用Closure钩子自己写。它不是手写 Mock 的完整替代品正如原文档所说它“能带你走完 90% 的路”对于超出“改改返回值”的复杂逻辑状态机、调用顺序编排、副作用模拟仍需手写或叠加 Closure 定制。下标subscript未完整支持模板的mockSubscript宏AutoMockable.stencil#L141-L147会生成完整签名的下标但 getter/setter 体内是fatalError(Subscripts are not fully supported yet)见 SubscriptProtocolMock。泛型类型化抛出未完整支持initE ... throws(E)、func doOrRethrowsE ... throws(E)等会生成fatalError占位TypedThrowableProtocolMock需要自行补写实现。命名噪音为区分重载而拼接的“方法名 参数 返回类型”长命名会让生成的变量名偏长这是模板为正确性付出的代价属预期行为。总而言之AutoMockable是“约定优于配置”的典型协议继承或注解声明意图 → Sourcery 扫描 → 模板生成可读的 Mock 源码全程无运行时魔法生成的代码就在你的工程里可以随时检查、修改与断言。把这条流水线接入测试 target 的 build phase 或.sourcery.ymlwatch 模式后协议更新时 Mock 也会同步更新测试维护成本会显著下降。赞分享代码生成开发工具【免费下载链接】SourceryMeta-programming for Swift, stop writing boilerplate code.项目地址https://gitcode.com/gh_mirrors/so/Sourcery点击查看免费下载相关推荐使用 Claudian 时哪些数据会离开本机API 请求与 Collab LAN 流量说明使用 Claudian 时哪些数据会离开本机API 请求与 Collab LAN 流量说明 Claudian 是一个把 Claude Code、Codex、G代码生成开发工具用 Sourcery AutoHashable 模板自动生成 Swift Hashable 实现协议标记驱动告别手写样板代码用 Sourcery AutoHashable 模板自动生成 Swift Hashable 实现协议标记驱动告别手写样板代码 本文以 Sourcery 仓库代码生成开发工具Sourcery 元编程实战指南用模板自动生成 Swift 样板代码Sourcery 元编程实战指南用模板自动生成 Swift 样板代码 Sourcery 是一款面向 Swift 的元编程代码生成器它扫描你的源码、应用你编写代码生成开发工具上一篇WebSocket库Wasm编译浏览器端WebSocket开发终极指南下一篇SMUDebugTool硬件调试利器助力AMD平台性能优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表