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一键生成PPT,就用博思AIPPT!
博思AIPPT,新一代的AI生成PPT平台,支持智能生成PPT、AI美化PPT、文本&链接生成PPT、导入Word/PDF/Markdown文档生成PPT等,内置海量精美PPT模板,涵盖商务、教育、科技等不同风格,同时针对每个页面提供多种版式,一键自适应切换,完美适配各种办公场景。


AI赋能电商视觉革命,一站式智能商拍平台
潮际好麦深耕服装行业,是国内AI试衣效果最好的软件。使用先进AIGC能力为电商卖家批量提供优质的、低成本的商拍图。合作品牌有Shein、Lazada、安踏、百丽等65个国内外头部品牌,以及国内10万+淘宝、天猫、京东等主流平台的品牌商家,为卖家节省将近85%的出图成本,提升约3倍出图效率,让品牌能够快速上架。


企业专属的AI法律顾问
iTerms是法大大集团旗下法律子品牌,基于最先进的大语言模型(LLM)、专业的法律知识库和强大的智能体架构,帮助企业扫清合规障碍,筑牢风控防线,成为您企业专属的AI法律顾问。


稳定高效的流量提升解决方案,助力品牌曝光
稳定高效的流量提升解决方案,助力品牌曝光


最新版Sora2模型免费使用,一键生成无水印视频
最新版Sora2模型免费使用,一键生成无水印视频


实时语音翻译/同声传译工具
Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。


选题、配图、成文,一站式创作,让内容运营更高效
讯飞绘文,一个AI集成平 台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。


AI辅助编程,代码自动修复
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

