关于 Mock 'n' Roll
Mock 'n' Roll 是一个 Swift 的模拟库,它帮助您在单元测试时模拟功能。您可以使用完全支持参数调用的 invoke
方法调用,为任何函数 register
返回值,以及在您的模拟上进行 inspect
执行。
Mock 'n' Roll 支持模拟具有可选和非可选返回值的函数,以及无返回值的函数。它支持值、结构体、类和枚举,不对您编写的代码施加任何限制。
创建模拟
假设您有以下协议
protocol TestProtocol {
func functionWithIntResult(arg1: String, arg2: Int) -> Int
func functionWithStringResult(arg1: String, arg2: Int) -> String
func functionWithStructResult(arg1: String, arg2: Int) -> User
func functionWithClassResult(arg1: String, arg2: Int) -> Thing
func functionWithOptionalIntResult(arg1: String, arg2: Int) -> Int?
func functionWithOptionalStringResult(arg1: String, arg2: Int) -> String?
func functionWithOptionalStructResult(arg1: String, arg2: Int) -> User?
func functionWithOptionalClassResult(arg1: String, arg2: Int) -> Thing?
func functionWithVoidResult(arg1: String, arg2: Int)
}
要模拟 TestProtocol
,只需创建一个继承自 Mock
并实现 TestProtocol
的类
class TestClass: Mock, TestProtocol {
func functionWithIntResult(arg1: String, arg2: Int) -> Int { ... }
func functionWithStringResult(arg1: String, arg2: Int) -> String { ... }
func functionWithStructResult(arg1: String, arg2: Int) -> User { ... }
func functionWithClassResult(arg1: String, arg2: Int) -> Thing { ... }
func functionWithOptionalIntResult(arg1: String, arg2: Int) -> Int? { ... }
func functionWithOptionalStringResult(arg1: String, arg2: Int) -> String? { ... }
func functionWithOptionalStructResult(arg1: String, arg2: Int) -> User? { ... }
func functionWithOptionalClassResult(arg1: String, arg2: Int) -> Thing? { ... }
func functionWithVoidResult(arg1: String, arg2: Int) {}
为了使用模拟类,您必须
- 在每个函数中调用
invoke
以记录所有函数调用、它们的参数和返回值 - 在您要测试的任何函数上,让您的测试
register
所需的任何返回值 - 在您的测试中检查模拟的
executions
以断言测试是否成功执行
调用函数调用
每个模拟函数必须调用 invoke
以记录函数调用,包括输入参数和返回值。对于上面的 TestClass
,看起来可能像这样
class TestClass: Mock, TestProtocol {
func functionWithIntResult(arg1: String, arg2: Int) -> Int {
return invoke(functionWithIntResult, args: (arg1, arg2))
}
func functionWithStringResult(arg1: String, arg2: Int) -> String {
return invoke(functionWithStringResult, args: (arg1, arg2))
}
func functionWithStructResult(arg1: String, arg2: Int) -> User {
return invoke(functionWithStructResult, args: (arg1, arg2))
}
func functionWithClassResult(arg1: String, arg2: Int) -> Thing {
return invoke(functionWithClassResult, args: (arg1, arg2))
}
func functionWithOptionalIntResult(arg1: String, arg2: Int) -> Int? {
return invoke(functionWithOptionalIntResult, args: (arg1, arg2))
}
func functionWithOptionalStringResult(arg1: String, arg2: Int) -> String? {
return invoke(functionWithOptionalStringResult, args: (arg1, arg2))
}
func functionWithOptionalStructResult(arg1: String, arg2: Int) -> User? {
return invoke(functionWithOptionalStructResult, args: (arg1, arg2))
}
func functionWithOptionalClassResult(arg1: String, arg2: Int) -> Thing? {
return invoke(functionWithOptionalClassResult, args: (arg1, arg2))
}
func functionWithVoidResult(arg1: String, arg2: Int) {
invoke(functionWithVoidResult, args: (arg1, arg2))
}
}
无返回值的函数只需调用 invoke
,而有返回值的函数必须调用 return invoke
。
每当您的单元测试接触这些函数中的任何一个时,现在您将能够检查记录的函数调用,以验证模拟是否按预期调用。
注册值
对于返回非可选值的函数,你的测试必须在触摸模拟函数之前注册实际返回值。未能这样做将使你的测试崩溃,并引发preconditionFailure
。
你可以通过调用模拟的registerResult(for:result:)
函数来注册返回值,如下所示
let mock = TestClass()
mock.registerResult(for: mock.functionWithIntResult) { _ in return 123 }
由于结果块接受与实际函数相同的参数,因此您可以根据输入参数返回不同的结果值
let mock = TestClass()
mock.registerResult(for: mock.functionWithIntResult) { _, arg2 in return arg2 }
mock.registerResult(for: mock.functionWithStringResult) { arg1, _ in return arg1 }
对于返回void或可选值的函数,您可以不注册返回值,但只要您想影响测试,就应该注册。
断言模拟执行
为了验证模拟是否接收了预期的函数调用,您可以使用executions(for:)
获取有关一个函数接收调用次数、输入参数和返回结果的信息
_ = mock.functionWithIntResult(arg1: "abc", arg2: 123)
_ = mock.functionWithIntResult(arg1: "abc", arg2: 456)
_ = mock.functionWithIntResult(arg1: "abc", arg2: 789)
_ = mock.functionWithStringResult(arg1: "abc", arg2: 123)
_ = mock.functionWithStringResult(arg1: "def", arg2: 123)
let intExecutions = mock.executions(of: mock.functionWithIntResult)
let stringExecutions = mock.executions(of: mock.functionWithStringResult)
expect(intExecutions.count).to(equal(3))
expect(stringExecutions.count).to(equal(2))
expect(intExecutions[0].arguments.0).to(equal("abc"))
expect(intExecutions[0].arguments.1).to(equal(123))
expect(intExecutions[1].arguments.0).to(equal("abc"))
expect(intExecutions[1].arguments.1).to(equal(456))
expect(intExecutions[2].arguments.0).to(equal("abc"))
expect(intExecutions[2].arguments.1).to(equal(789))
expect(stringExecutions[0].arguments.0).to(equal("abc"))
expect(stringExecutions[0].arguments.1).to(equal(123))
expect(stringExecutions[1].arguments.0).to(equal("def"))
expect(stringExecutions[1].arguments.1).to(equal(123))
注意,上面的代码使用了Quick/Nimble,以防您不熟悉该语法。
注册和引发错误
目前没有注册和引发错误的支持,这意味着异步函数(目前)无法注册自定义返回值。在此功能实施之前,您可以使用Mock
类的error
属性。
安装
CocoaPods
使用CocoaPods安装Mock 'n' Roll,请在您的Podfile
文件中添加以下内容:
pod 'MockNRoll'
Carthage
使用Cartfile
文件中添加以下内容:
github "danielsaidi/MockNRoll"
手动安装
如果您不想使用Carthage或CocoaPods,可以将Mock 'n' Roll添加到您的应用中。首先克隆此存储库并将它放置在磁盘上的某个位置,然后将MockNRoll.xcodeproj
添加到项目,并将MockNRoll.framework
作为嵌入的应用程序二进制文件和目标依赖项添加。
重要的设备限制
Mock 'n' Roll使用不安全位转换来获取模拟函数的内存地址。这仅在64位设备上工作,这意味着基于模拟的单元测试在旧设备或模拟器(如iPad 2、iPad Retina等)上无法正确运行。
联系方式
希望您喜欢这个库。如果您有任何问题或想以任何方式做出贡献,请随时联系我。
- 邮箱:[email protected]
- Twitter:@danielsaidi
- 网站:danielsaidi.com
鸣谢
Mock 'n' Roll受到Stubber的启发,但是拥有一个完全独立的代码库。虽然Stubber使用全局函数(这需要你时不时地清空执行存储),但Mock 'n' Roll将此逻辑移到每个模拟中,这意味着当模拟被销毁时,任何记录的执行都会自动清除。Mock 'n' Roll还增加了额外的功能,比如可选和空结果支持。
许可证
Mock 'n' Roll遵循MIT许可证。有关更多信息,请参阅LICENSE文件。