Mockable是一个由Swift宏驱动的测试框架,为您的协议提供自动生成的模拟实现。它提供了直观的声明式语法,简化了在单元测试中模拟服务的过程。生成的模拟实现可以使用编译条件从发布版本中排除。
我将Mockable作为一个有趣的副项目开发。我相信开源的力量,很高兴与您分享它。如果Mockable为您节省了时间,并且您想表示感谢,我将非常感激您的支持。
<a href="https://www.buymeacoffee.com/kolos"><img src="https://img.buymeacoffee.com/button-api/?text=给我买杯啤酒&emoji=🍺&slug=kolos&button_colour=FF5F5F&font_colour=ffffff&font_family=Arial&outline_colour=000000&coffee_colour=FFDD00" /></a>
阅读Mockable的文档,了解详细的安装和配置指南以及使用示例。
该库可以使用Swift Package Manager安装。
Mockable提供两个库产品:
@Mockable
宏的核心库。XCTest
框架的测试工具(仅与测试目标链接)。使用该库:
阅读文档中的安装指南,了解如何将Mockable与您的项目集成的更多详细信息。
由于@Mockable
是一个对等宏,
生成的代码将始终与附加到的协议处于相同的作用域。
为了解决这个问题,宏展开被封装在一个预定义的编译时标志**MOCKING
**中,可以利用它来从发布版本中排除生成的模拟实现。
⚠️ 由于**
MOCKING
**标志在您的项目中默认未定义,除非您配置它,否则您将无法使用模拟实现。
在目标的构建设置中为调试构建配置定义标志:
在包清单的目标定义下,如果构建配置设置为**debug
,您可以定义MOCKING
**编译时条件:
.target( ... swiftSettings: [ .define("MOCKING", .when(configuration: .debug)) ] )
在您的XcodeGen规范中定义标志:
settings: ... configs: debug: SWIFT_ACTIVE_COMPILATION_CONDITIONS: MOCKING
如果您使用Tuist,可以在目标设置的configurations
下定义MOCKING
标志。
.target( ... settings: .settings( configurations: [ .debug( name: .debug, settings: [ "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING" ] ) ] ) )
阅读文档中的配置指南,了解如何在项目中设置**MOCKING
**标志的更多详细信息。
给定一个用@Mockable
宏注释的协议:
import Mockable @Mockable protocol ProductService { var url: URL? { get set } func fetch(for id: UUID) async throws -> Product func checkout(with product: Product) throws }
将生成一个名为MockProductService
的模拟实现,可以在单元测试中这样使用:
import MockableTest lazy var productService = MockProductService() lazy var cartService = CartServiceImpl(productService: productService) func testCartService() async throws { let mockURL = URL(string: "apple.com") let mockError: ProductError = .notFound let mockProduct = Product(name: "iPhone 15 Pro") given(productService) .fetch(for: .any).willReturn(mockProduct) .checkout(with: .any).willThrow(mockError) try await cartService.checkout(with: mockProduct, using: mockURL) verify(productService) .fetch(for: .value(mockProduct.id)).called(.atLeastOnce) .checkout(with: .value(mockProduct)).called(.once) .url(newValue: .value(mockURL)).setCalled(.once) }
Mockable使用声明式语法,利用构建器来构造given
、when
和verify
子句。
在构造这些子句时,您始终遵循相同的语法:
子句类型
(服务
).函数构建器
.行为构建器
在以下示例中,我们使用之前介绍的产品服务:
let id = UUID() let error: ProductError = .notFound given(productService).fetch(for: .value(id)).willThrow(error)
我们指定以下内容:
given
:我们想要注册返回值(productService)
:我们指定要为哪个可模拟服务注册返回值.fetch(for: .value(id))
:我们想要模拟fetch(for:)
方法,并将行为限制在匹配id
参数的调用上.willThrow(error)
:如果使用指定的参数值调用fetch(for:)
,我们希望抛出一个错误函数构建器保留了原始需求的所有参数,但将它们封装在Parameter<Value>
类型中。
在构建可模拟子句时,你需要为函数的每个参数指定参数条件。有三个可用选项:
.any
:匹配对指定函数的每次调用,忽略实际参数值。.value(Value)
:匹配在指定参数中具有相同值的调用。.matching((Value) -> Bool)
:使用提供的闭包来过滤函数调用。计算属性没有参数,但可变属性在函数构建器中会获得一个
(newValue:)
参数,可用于通过匹配条件约束属性赋值的功能。这些newValue
条件只会影响performOnGet
、performOnSet
、getCalled
和setCalled
子句,但不会影响返回子句。
以下是使用不同参数条件的示例:
// 当使用`id`调用`fetch(for:)`时抛出错误 given(productService).fetch(for: .value(id)).willThrow(error) // 如果产品服务被调用时传入名为"iPhone 15 Pro"的产品,则打印"Ouch!" when(productService) .checkout(with: .matching { $0.name == "iPhone 15 Pro" }) .perform { print("Ouch!") } // 断言fetch(for:)是否恰好被调用一次,不管它被调用时使用了什么id参数 verify(productService).fetch(for: .any).called(.once)
可以使用given(_ service:)
子句指定返回值。有三种可用的返回构建器:
willReturn(_ value:)
:将存储给定的返回值并用于模拟后续调用。willThrow(_ error:)
:将存储给定的错误并在后续调用中抛出。仅适用于可抛出的函数和属性。willProduce(_ producer)
:将使用提供的闭包进行模拟。闭包的签名与被模拟的函数相同,例如,一个接受整数、返回字符串并可能抛出错误的函数将接受类型为(Int) throws -> String
的闭包。提供的返回值按先进先出(FIFO)顺序使用,最后一个始终保留用于任何进一步的调用。以下是使用返回子句的示例:
// 第一次调用时抛出错误,然后每次都返回'product' given(productService) .fetch(for: .any).willThrow(error) .fetch(for: .any).willReturn(product) // 如果id参数以0结尾则抛出错误,否则返回产品 given(productService) .fetch(for: .any).willProduce { id in if id.uuidString.last == "0" { throw error } else { return product } }
可以使用when(_ service:)
子句添加副作用。有三种副作用:
perform(_ action)
:将注册一个操作,在模拟函数被调用时执行。performOnGet(_ action:)
:仅适用于可变属性,将在属性被访问时执行提供的操作。performOnSet(_ action:)
:仅适用于可变属性,将在属性被赋值时执行提供的操作。以下是使用副作用的一些示例:
// 记录fetch(for:)的调用 when(productService).fetch(for: .any).perform { print("fetch(for:)被调用") } // 记录url被访问时 when(productService).url().performOnGet { print("url被访问") } // 记录url被设置为nil时 when(productService).url(newValue: .value(nil)).performOnSet { print("url被设置为nil") }
你可以使用verify(_ service:)
子句验证模拟服务的调用。
有三种验证方式:
called(_:)
:根据给定值断言调用次数。getCalled(_:)
:仅适用于可变属性,断言属性访问次数。setCalled(_:)
:仅适用于可变属性,断言属性赋值次数。以下是一些断言示例:
verify(productService) // 断言fetch(for:)被调用1到5次 .fetch(for: .any).called(.from(1, to: 5)) // 断言checkout(with:)恰好被调用10次 .checkout(with: .any).called(10) // 断言url属性至少被访问2次 .url().getCalled(.moreOrEqual(to: 2)) // 断言url属性从未被设置为nil .url(newValue: .value(nil)).setCalled(.never)
如果你正在测试异步代码且无法编写同步断言,可以使用上述验证的异步对应版本:
calledEventually(_:before:)
:等待直到超时或满足调用次数。getCalledEventually(_:before:)
:等待直到超时或满足属性访问次数。setSalledEventually(_:before:)
:等待直到超时或满足属性赋值次数。以下是一些异步验证的示例:
await verify(productService) // 断言fetch(for:)在默认超时时间(1秒)内被调用1到5次 .fetch(for: .any).calledEventually(.from(1, to: 5)) // 断言checkout(with:)在3秒内恰好被调用10次 .checkout(with: .any).calledEventually(10, before: .seconds(3)) // 断言url属性在默认超时时间(1秒)内至少被访问2次 .url().getCalledEventually(.moreOrEqual(to: 2)) // 断言url属性被设置为nil一次 .url(newValue: .value(nil)).setCalledEventually(.once)
默认情况下,你必须为所有需求指定返回值;否则,将抛出致命错误。这样做的原因是为了在编写单元测试时帮助发现(从而验证)每个被调用的函数。
然而,通常更倾向于避免这种严格的默认行为,转而采用更宽松的设置,例如,void或可选返回值不需要显式的given
注册。
使用 MockerPolicy
(这是一个选项集)来隐式模拟:
.relaxedMockable
[.relaxedVoid, .relaxedOptional]
.relaxed
你有两种选择来覆盖库的默认严格行为:
let relaxedMock = MockService(policy: [.relaxedOptional, .relaxedVoid])
MockerPolicy.default = .relaxedVoid
.relaxedMockable
策略与 Mockable
协议结合使用,可以为自定 义(甚至内置)类型设置隐式返回值:
struct Car { var name: String var seats: Int } extension Car: Mockable { static var mock: Car { Car(name: "Mock Car", seats: 4) } // 默认为 [mock],但我们可以 // 提供自定义的汽车数组: static var mocks: [Car] { [ Car(name: "Mock Car 1", seats: 4), Car(name: "Mock Car 2", seats: 4) ] } } @Mockable protocol CarService { func getCar() -> Car func getCars() -> [Car] } func testCarService() { func test() { let mock = MockCarService(policy: .relaxedMockable) // 无需显式注册即可隐式模拟: let car = mock.getCar() let cars = mock.getCars() } }
⚠️ 放松模式不适用于泛型返回值,因为类型系统无法定位适当的泛型重载。
Mockable 内部使用 Matcher
来比较参数。
默认情况下,匹配器能够比较任何遵循 Equatable
的自定义类型(在泛型函数中使用时除外)。
在特殊情况下,当你
你可以使用 Matcher.register()
函数注册你的自定义类型。
以下是如何操作:
// 将可比较类型注册到匹配器,因为我们在泛型函数中使用它 Matcher.register(SomeEquatableType.self) // 将非可比较类型注册到匹配器 Matcher.register(Product.self, match: { $0.name == $1.name }) // 将元类型注册到匹配器 Matcher.register(HomeViewController.Type.self) // 移除所有先前注册的自定义类型 Matcher.reset()
如果你在测试期间看到类似这样的错误:
未找到类型"SomeType"的比较器。所有非可比较类型必须使用 Matcher.register(_) 进行注册。
记得使用 register()
函数将注明的类型添加到你的 Matcher
中。
如果你遇到任何项目问题或有改进建议,请随时提出问题。我重视你的反馈,并致力于使这个项目尽可能健壮和用户友好。
包清单设置为仅在名为 MOCKABLE_DEV
的环境变量设置为 true 时才包含测试目标和测试依赖项。这样做是为了防止过于热心的 Swift Package Manager 在有人将 Mockable 用作包依赖项时下载测试依赖项和插件,如 swift-macro-testing
或 SwiftLint
。
要在"开发模式"下用 Xcode 打开包,你需要设置 MOCKABLE_DEV=true
环境变量。使用 Scripts/open.sh
打开项目(或将其内容复制到你的终端)以便在贡献时运行测试和检查代码。
Mockable 根据 MIT 许可证提供。请查看 LICENSE 文件了解更多详情。
AI数字人视频创作平台
Keevx 一款开箱即用的AI数字人视频创作平台,广泛适用于电商广告、企业培训与社媒宣传,让全球企业与个人创作者无需拍摄剪辑,就能快速生成多语言、高质量的专业视频。
一站式AI创作平台
提供 AI 驱动的图片、视频生成及数字人等功能,助力创意创作
AI办公助手,复杂任务高效处理
AI办公助手,复杂任务高效处理。办公效率低?扣子空间AI助手支持播客生成、PPT制作、网页开发及报告写作,覆盖科研、商业、舆情等领域的专家Agent 7x24小时响应,生活工作无缝切换,提升50%效率!
AI辅助编程,代码自动修复
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。
AI小说写作助手,一站式润色、改写、扩写
蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。
全能AI智能助手,随时解答生活与工作的多样问题
问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。
实时语音翻译/同声传译工具
Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。
一键生成PPT和Word,让学习生活更轻松
讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。
深度推理能力全新升级,全面对标OpenAI o1
科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。
一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型
Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。
最新AI工具、AI资讯
独家AI资源、AI项目落地
微信扫一扫关注公众号