Quick 共享示例(Shared Examples)与 Behavior:用共享断言消除测试样板代码
Quick 共享示例Shared Examples与 Behavior用共享断言消除测试样板代码【免费下载链接】QuickThe Swift (and Objective-C) testing framework.项目地址: https://gitcode.com/gh_mirrors/qu/Quick在 Swift/Objective-C 测试中同一套断言常常需要针对多个不同对象重复执行。Quick 为此提供了两套机制——基于字符串命名的sharedExamples共享示例以及基于泛型的BehaviorContext——让你把公共断言定义一次、处处复用。本文以海豚吃掉可食用对象会变开心为例完整讲解这两种写法的 Swift 与 Objective-C 用法并结合 Quick 源码World、DSL、测试用例说明其注册、展开与执行原理读完后你可以直接在自己的 Spec 中落地共享测试。典型场景同一断言适用于多个对象假设业务代码中存在一个协议Edible可食用当海豚Dolphin吃掉一个Edible对象后海豚会变得开心。Mackerel鲭鱼和Cod鳕鱼都遵循Edibleprotocol Edible { } class Dolphin { private(set) var isHappy: Bool init(happy: Bool) { self.isHappy happy } /// Makes Dolphin happy func eat(_ edible: Edible) { isHappy true } } class Mackerel: Edible { } class Cod: Edible { }现在要验证海豚无论吃鲭鱼还是鳕鱼都会开心。如果为Mackerel和Cod各写一份完整 Spec断言逻辑会完全重复。Quick 的目标就是让这种重复降到最低把吃下某物后开心这一组断言抽出来分别喂给不同的被测对象。方式一sharedExamplesitBehavesLike按名字共享最经典的共享示例 API 由两个函数组成sharedExamples(名字) { context in ... }定义一组可复用的示例闭包内可以包含任意数量的it、context、describe以及beforeEach/afterEachitBehavesLike(名字) { ... }在某个 Spec 的当前位置展开这组共享示例并可通过闭包传入上下文参数。在 Swift 中共享示例定义在QuickConfiguration的子类里以便在任何示例运行前完成注册对应源码见 Sources/Quick/Configuration/QuickConfiguration.swift 中configure(_:)的说明。Swift 完整示例// Swift import Quick import Nimble class EdibleSharedExamplesConfiguration: QuickConfiguration { override class func configure(_ configuration: Configuration) { sharedExamples(something edible) { (sharedExampleContext: escaping SharedExampleContext) in it(makes dolphins happy) { let dolphin Dolphin(happy: false) let edible sharedExampleContext()[edible] dolphin.eat(edible) expect(dolphin.isHappy).to(beTruthy()) } } } } class MackerelSpec: QuickSpec { override class func spec() { var mackerel: Mackerel! beforeEach { mackerel Mackerel() } itBehavesLike(something edible) { [edible: mackerel] } } } class CodSpec: QuickSpec { override class func spec() { var cod: Cod! beforeEach { cod Cod() } itBehavesLike(something edible) { [edible: cod] } } }关键点在于数据流动方式itBehavesLike(something edible) { [edible: mackerel] }的闭包返回一个[String: Any]字典共享示例内部通过sharedExampleContext()[edible]取出对应的被测对象。SharedExampleContext的类型定义位于 Sources/Quick/World.swift/// A closure that, when evaluated, returns a dictionary of key-value /// pairs that can be accessed from within a group of shared examples. public typealias SharedExampleContext () - [String: Any] /// A closure that is used to define a group of shared examples. public typealias SharedExampleClosure (escaping SharedExampleContext) - Void也就是说上下文是一个求值后返回字典的闭包这保证了即使在beforeEach之后才真正求值拿到的也是当次运行的最新对象例如上面beforeEach中新构造的mackerel。Objective-C 完整示例Objective-C 侧使用QuickConfigurationBegin/QuickConfigurationEnd宏声明配置类QuickSpecBegin/QuickSpecEnd宏声明 Spec 类共享示例与itBehavesLike的调用形式与 Swift 对应宏定义见 Sources/QuickObjectiveC/DSL/QCKDSL.h// Objective-C import Quick; import Nimble; QuickConfigurationBegin(EdibleSharedExamplesConfiguration) (void)configure:(Configuration *configuration) { sharedExamples(something edible, ^(QCKDSLSharedExampleContext exampleContext) { it(makes dolphins happy, ^{ Dolphin *dolphin [[Dolphin alloc] init]; dolphin.happy NO; idEdible edible exampleContext()[edible]; [dolphin eat:edible]; expect(dolphin.isHappy).to(beTruthy()) }); }); } QuickConfigurationEnd QuickSpecBegin(MackerelSpec) __block Mackerel *mackerel nil; beforeEach(^{ mackerel [[Mackerel alloc] init]; }); itBehavesLike(something edible, ^{ return { edible: mackerel }; }); QuickSpecEnd QuickSpecBegin(CodSpec) __block Cod *cod nil; beforeEach(^{ cod [[Cod alloc] init]; }); itBehavesLike(something edible, ^{ return { edible: cod }; }); QuickSpecEnd说明日文原版文档中 CodSpec 的声明写成了__block Mackerel *cod属于类型笔误本文按英文版语义修正为Cod *cod同时英文版文档使用的共享示例名为something edible日文版中误写为someting edible实际使用时两者必须完全一致见下文名字唯一性。方式二BehaviorContext类型安全的共享示例sharedExamples以字符串为键、以[String: Any]字典传参灵活但缺乏类型检查。Quick 提供了基于 Swift 泛型的BehaviorContext把上下文类型化编译期就能发现类型错误。其定义在 Sources/Quick/Behavior.swiftopen class BehaviorContext { open class var name: String { return String(describing: self) } open class func spec(_ aContext: escaping () - Context) {} }用法是把公共断言写进Behavior子类的spec(_:)静态方法再在 Spec 里用itBehavesLike(BehaviorType.self) { 上下文 }展开class SomethingEdible: BehaviorEdible { override class func spec(_ aContext: escaping () - Edible) { var edible: Edible! beforeEach { edible aContext() } it(makes dolphins happy) { let dolphin Dolphin(happy: false) dolphin.eat(edible) expect(dolphin.isHappy).to(beTruthy()) } } } class MackerelSpec: QuickSpec { override class func spec() { var mackerel: Mackerel! beforeEach { mackerel Mackerel() } itBehavesLike(SomethingEdible.self) { mackerel } } } class CodSpec: QuickSpec { override class func spec() { var cod: Cod! beforeEach { cod Cod() } itBehavesLike(SomethingEdible.self) { cod } } }这里BehaviorEdible的泛型参数是Edible闭包{ mackerel }返回的就是Edible类型由编译器保证。由于BehaviorContext使用了 Swift 泛型它无法从 Objective-C 使用——Objective-C 只能走sharedExamples字符串方案。源码视角共享示例如何注册、查找与展开理解底层机制有助于排查找不到共享示例名字重复等常见问题。三个关键环节都集中在 Sources/Quick/World.swift 与 Sources/Quick/DSL/WorldDSL.swift。1. 注册名字必须全局唯一顶层sharedExamples(name) { ... }最终调用World.sharedWorld.sharedExamples(name, closure:)进入registerSharedExampleSources/Quick/World.swiftinternal func registerSharedExample(_ name: String, closure: escaping SharedExampleClosure) { raiseIfSharedExampleAlreadyRegistered(name) sharedExamples[name] closure }所有共享示例集中存放在World的private var sharedExamples: [String: SharedExampleClosure] [:]字典中。如果重复注册同名示例会触发raiseIfSharedExampleAlreadyRegistered抛出异常如果itBehavesLike引用了未注册的名字raiseIfSharedExampleNotRegistered会抛出异常并列出所有已注册的名字private func raiseIfSharedExampleNotRegistered(_ name: String) { if sharedExamples[name] nil { raiseError(No shared example named \(name) has been registered. Registered shared examples: \(Array(sharedExamples.keys))) } }因此定义名字与引用名字必须逐字符一致这是最常见的出错点。2. 展开把共享示例内联进当前示例组itBehavesLike的底层实现在 Sources/Quick/DSL/WorldDSL.swift。它按名字取出已注册的闭包以共享示例名新建一个ExampleGroup挂到当前示例组下然后在新组中执行闭包nonobjc internal func itBehavesLike(_ name: String, sharedExampleContext: escaping SharedExampleContext, flags: FilterFlags [:], file: FileString, line: UInt) { guard currentExampleMetadata nil else { raiseError(itBehavesLike cannot be used inside \(currentPhase), itBehavesLike may only be used inside context or describe.) } let callsite Callsite(file: file, line: line) let closure World.sharedWorld.sharedExample(name) let group ExampleGroup(description: name, flags: flags) currentExampleGroup.appendExampleGroup(group) performWithCurrentExampleGroup(group) { closure(sharedExampleContext) } group.walkDownExamples { (example: Example) in example.isSharedExample true example.callsite callsite } }这段代码揭示了三个实现事实共享示例本质上是就地展开执行时如同把it等语句手动写进当前位置展开后组内每个Example会被标记为isSharedExample true并把callsite指向调用itBehavesLike的位置这样失败定位会指向调用处而非共享示例定义处itBehavesLike只能出现在context或describe内部若在it块内调用会抛出itBehavesLike cannot be used inside it, itBehavesLike may only be used inside context or describe.异常——这一点被测试 Tests/QuickTests/QuickTests/FunctionalTests/SharedExamplesTests.swift 显式验证。BehaviorContext的展开路径与之对称WorldDSL.swift 中itBehavesLike(_ behavior:context:...)直接用behavior.spec作为闭包、behavior.name作为组名同样标记isSharedExample并重写callsite。3. 配置时机共享示例在示例运行前完成注册QuickConfiguration子类的configure(_:)会在 Quick 运行任何示例之前被统一调用见 Sources/Quick/Configuration/QuickConfiguration.swift 的_configureSubclassesIfNeeded遍历所有QuickConfiguration子类依次执行configure随后world.finalizeConfiguration()冻结配置。因此在 configure 中注册共享示例可以保证后续所有 Spec 执行itBehavesLike时都能查到对应名字。测试固件 Tests/QuickTests/QuickTests/Fixtures/FunctionalTests_SharedExamplesTests_SharedExamples.swift 就是这样一个典型的跨文件共享示例注册点。共享示例可以包含什么it、context、describe 与 Hook共享示例闭包表现得就像一个用describe/context定义的示例组Sources/Quick/DSL/DSL.swift 中sharedExamples的文档注释因此可以包含任意数量的it示例嵌套的context/describe示例组beforeEach/afterEach/justBeforeEach/aroundEach等 hook对应闭包类型见 Sources/Quick/Hooks/Closures.swift。仓库测试 Tests/QuickTests/QuickTests/FunctionalTests/SharedExamplesBeforeEachTests.swift 直接验证了 hook 的执行语义class FunctionalTests_SharedExamples_BeforeEachTests_SharedExamples: QuickConfiguration { override class func configure(_ configuration: QCKConfiguration) { sharedExamples(a group of three shared examples with a beforeEach) { beforeEach { sharedExamplesBeforeEachExecutedCount 1 } it(passes once) {} it(passes twice) {} it(passes three times) {} } } } class FunctionalTests_SharedExamples_BeforeEachSpec: QuickSpec { override class func spec() { beforeEach { specBeforeEachExecutedCount 1 } it(executes the spec beforeEach once) {} itBehavesLike(a group of three shared examples with a beforeEach) } }测试断言Spec 外层的beforeEach总共执行 4 次1 个普通示例 3 个共享示例而共享示例内部的beforeEach恰好执行 3 次每个共享示例一次。这印证了共享示例按调用点展开、内部 hook 随每个示例独立执行的语义。无上下文的共享示例测试全局状态有些场景不需要任何上下文参数。此时 Swift 可以直接使用不带参数的sharedExamples闭包这对测试某种全局状态很有用// Swift import Quick sharedExamples(everything under the sea) { // ... } itBehavesLike(everything under the sea)itBehavesLike有一个不接收 context 的重载它在内部自动补一个返回空字典的闭包public static func itBehavesLike(_ name: String, file: FileString #file, line: UInt #line) { itBehavesLike(name, file: file, line: line, sharedExampleContext: { return [:] }) }Objective-C 注意事项即使在 Objective-C 中不打算使用QCKDSLSharedExampleContext也必须传入一个接收该参数的 blocksharedExamples(name, ^(QCKDSLSharedExampleContext exampleContext) { ... })与itBehavesLike(name, ^NSDictionary *{ return {}; })这是 Objective-C 的硬性要求没有捷径。聚焦与跳过fitBehavesLike与xitBehavesLike与fit/xit对应Quick 为共享示例提供了聚焦与跳过变体定义见 Sources/Quick/DSL/DSL.swiftfitBehavesLike(...)在itBehavesLike前加f当测试套件中存在任何被聚焦的示例时只运行被聚焦的部分共享示例也随之只跑聚焦的那份xitBehavesLike(...)把共享示例标记为 pending不执行其内部代码。三者都有按名字 上下文与按Behavior类型两套重载例如fitBehavesLike(something edible) { [edible: mackerel] } fitBehavesLike(SomethingEdible.self) { mackerel }底层实现上fitBehavesLike只是给itBehavesLike附加[Filter.focused: true]标志、xitBehavesLike附加[Filter.pending: true]标志见 Sources/Quick/DSL/WorldDSL.swift随后由World.includedExamples()的过滤逻辑决定哪些示例真正执行。Quick 自身的聚焦测试 Tests/QuickTests/QuickFocusedTests/FocusedTests.swift 中同时使用了字符串与Behavior两种fitBehavesLike形式Objective-C 侧见 Tests/QuickTests/QuickFocusedTests/FocusedTestsObjC.m。两种方式如何选择维度sharedExamplesitBehavesLikeBehaviorContext语言支持Swift 与 Objective-C仅 Swift依赖泛型上下文类型[String: Any]字典无类型检查泛型Context编译期类型安全命名方式字符串名字全局唯一类名可覆写name定义位置QuickConfiguration.configure任何可访问的Behavior子类适用建议跨语言复用、简单共享场景追求类型安全、共享逻辑较复杂从源码注释看Quick 官方在 DSL.swift 中明确标注sharedExamples是无类型的sharedExamplesis untyped. Please useBehaviorinstead, as it offers type-safety.推荐新代码优先使用Behavior而涉及 Objective-C 或需要运行时动态命名的场景则继续使用字符串方案。常见陷阱小结名字不一致itBehavesLike引用的名字必须与sharedExamples注册的名字逐字符一致否则运行前抛出异常名字重复注册同一个共享示例名只能注册一次重复注册会崩溃调用位置错误itBehavesLike只能在context/describe内使用放进it块会抛出异常测试用例见 SharedExamplesTests.swift 与 BehaviorTests.swiftObjective-C 的 block 签名即使不用上下文也必须提供接收QCKDSLSharedExampleContext的 block配置时机确保注册共享示例的QuickConfiguration子类能被测试 target 发现跨文件共享示例通常集中放在一个配置文件中仓库的 Fixtures/FunctionalTests_SharedExamplesTests_SharedExamples.swift 即是这种组织方式。结语共享断言是 Quick 减少测试样板代码的核心手段sharedExamples提供跨语言的字符串命名复用BehaviorContext提供 Swift 侧的类型安全复用二者共用同一套注册—查找—展开机制。理解了 World.swift 中的注册表与 WorldDSL.swift 中的展开逻辑你就能准确预估共享示例的执行次数、hook 行为与失败定位方式在真实项目中放心地把公共断言抽成可复用的测试资产。【免费下载链接】QuickThe Swift (and Objective-C) testing framework.项目地址: https://gitcode.com/gh_mirrors/qu/Quick创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Humanizer InDate.Nine 全面解析:用 DateOnly 表达 9 天/9 周/9 个月/9 年后的日期

Humanizer InDate.Nine 全面解析:用 DateOnly 表达 9 天/9 周/9 个月/9 年后的日期

开发工具 【免费下载链接】Humanizer Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities 项目地址: https://gitcode.com/gh_mirrors/hu/Humanizer 点击查看 免费下载 导读 …

2026/9/24 17:00:10 阅读更多 →
gsd-core 配置读取修复解析:`config-get --default true` 如何消除 Nyquist 校验开关的 stderr 噪音与空变量回退

gsd-core 配置读取修复解析:`config-get --default true` 如何消除 Nyquist 校验开关的 stderr 噪音与空变量回退

gsd-core 配置读取修复解析:config-get --default true 如何消除 Nyquist 校验开关的 stderr 噪音与空变量回退 【免费下载链接】gsd-core Git. Ship. Done - Core 项目地址: https://gitcode.com/gh_mirrors/ge/gsd-core 本文以 gsd-core 仓库中归档变更集 .…

2026/9/24 16:59:10 阅读更多 →
Hermes Rust 工作区公共工具 crate:hermes_utils 设计规范与 PointerAddress 实现解析

Hermes Rust 工作区公共工具 crate:hermes_utils 设计规范与 PointerAddress 实现解析

语言运行时编译器移动开发 【免费下载链接】hermes A JavaScript engine optimized for running React Native. 项目地址: https://gitcode.com/gh_mirrors/hermes/hermes 点击查看 免费下载 hermes_utils 是 Hermes 项目 Rust 工作区中一个"兜底型"公共…

2026/9/24 16:59:10 阅读更多 →

最新新闻

JSP+MySQL个人日记本源码运行全攻略:从环境搭建到避坑指南

JSP+MySQL个人日记本源码运行全攻略:从环境搭建到避坑指南

简介:基于jspmysql的JSP个人日记本源码,是一份面向Java Web初学者与课程设计场景的完整Web应用项目。资源以JSP作为视图层、Servlet处理控制逻辑,结合MySQL存储用户、日记与分类数据,覆盖用户登录、会话保持、日记增删改查、分类管…

2026/9/24 18:29:14 阅读更多 →
2026年AirPods全系选购指南:从AirPods 4到Pro与Max,哪款最适合你

2026年AirPods全系选购指南:从AirPods 4到Pro与Max,哪款最适合你

AirPods这个品类,现在真的有点“乱花渐欲迷人眼”了。从最初只有一款标准版,到如今AirPods 4、AirPods Pro、AirPods Max三条线并行,Plus版一般、Lite版传闻满天飞,哪怕是数码圈老手,真要下手买前不花点时间做功课&…

2026/9/24 18:29:14 阅读更多 →
结构可靠性分析:验算点法与直接抽样组合实战

结构可靠性分析:验算点法与直接抽样组合实战

简介:面向结构可靠性与安全度评估的实用算法压缩包,适合结构工程专业学生、科研人员及从事可靠性分析的工程师。资源聚焦验算点法和直接抽样法,同时涉及蒙特卡洛模拟思路,用于计算结构失效概率、评估极端荷载下的安全裕度&#xf…

2026/9/24 18:29:14 阅读更多 →
前后端分离部署实战:systemd托管Java进程+Nginx反向代理

前后端分离部署实战:systemd托管Java进程+Nginx反向代理

1. 部署方案设计与总体思路1.1 为什么是“systemd Nginx”这套组合前后端分离项目的部署,说简单也简单,说麻烦也麻烦。简单在于流程是固定的——后端打成 jar 包跑起来,前端构建出静态文件扔给 Web 服务器。麻烦在于每一个环节都有坑&#x…

2026/9/24 18:29:14 阅读更多 →
腾讯云EdgeOne免费CDN实战:接入配置与成本优化指南

腾讯云EdgeOne免费CDN实战:接入配置与成本优化指南

上个月帮一个朋友的博客站点排查访问慢的问题。他的服务器带宽只有5M,文章里全是高清截图和录屏素材,高峰期十几个人同时打开就卡得不行。我顺手看了眼他用的还是老式按量计费CDN,月底账单里光是流量费就占了一半。我说你这个站,流…

2026/9/24 18:29:14 阅读更多 →
Linux与Windows系统运维:参数查询与配置命令对照实战

Linux与Windows系统运维:参数查询与配置命令对照实战

很多时候,我们干的活儿并不是什么高深莫测的架构设计,反而是那些每天都在重复的“查一下参数、改一个配置”。尤其是当你的手头同时管着 Windows 服务器和 Linux 服务器时,这种“精神分裂”的感觉会特别明显:明明在 Windows 上用图…

2026/9/24 18:28:13 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →