SwiftyMock
此仓库包含辅助程序,使在 Swift 中进行模拟、存根和监视变得更加容易
示例
协议
想象您有一个协议描述了一些行为
protocol RoboKitten {
func batteryStatus() -> Int
func jump(x x: Int, y: Int) -> Int
func canJumpAt(x x: Int, y: Int) -> Bool
func rest(completed: Bool -> () )
}
协议使用
这个协议被用在了某处
class RoboKittenController {
let kitten: RoboKitten
init(kitten: RoboKitten) {
self.kitten = kitten
}
func jumpAt(x x: Int, y: Int) -> Result {
if kitten.canJumpAt(x: x, y: y) {
kitten.jump(x: x, y: y)
return .SUCCESS
}
return .FAILURE
}
...
}
协议模拟
那么现在您想要测试该协议作为依赖项的使用情况
并创建协议的模拟实现
并为要测试的每个方法创建假的调用
那么在SwiftyMock中,它会看起来怎么样
class RoboKittenMock: RoboKitten {
let batteryStatusCall = FunctionCall<(), Int>()
func batteryStatus() -> Int {
return stubCall(batteryStatusCall, argument:())
}
let jumpCall = FunctionCall<(x: Int, y: Int), Int>()
func jump(x x: Int, y: Int) -> Int {
return stubCall(jumpCall, argument: (x: x, y: y))
}
let canJumpAtCall = FunctionCall<(x: Int, y: Int), Bool>()
func canJumpAt(x x: Int, y: Int) -> Bool {
return stubCall(canJumpAtCall, argument: (x: x, y: y))
}
let restCall = FunctionCall<Bool -> (), ()>()
func rest(completed: Bool -> ()) {
return stubCall(restCall, argument: completed, defaultValue: ())
}
}
模拟使用方法
如果设置正确,通过使用替换方法指定模拟行为非常简单
方法存根
假设我们希望模拟返回某些值
这非常简单
// like this
kittenMock.canJumpAtCall.returns(false)
// or like this
kittenMock.jumpCall.returns(20)
有时,您有更复杂的规则,确定何时以及返回什么
// You can add as many filters you like
// More specific rules overrides general rules
kittenMock.canJumpAtCall
.on { $0.x < 0 }.returns(false)
.on { $0.y < 0 }.returns(false)
.returns(true) // in all other cases
有时您可能需要更复杂的模拟行为,例如,需要将闭包传递到模拟中
protocol RoboKitten {
func rest(completed: Bool -> () )
}
kittenMock.restCall.performs { completion in
print("Mock method was called! Remove this in prod version:))")
completion(true)
}
方法调用检查
有时候,您需要确保方法被调用
beforeEach {
// Since canjump method need to return somtehing we need to specify return value
kittenMock.canJumpAtCall.returns(false)
}
it("should ask kitten if it's available to jump there") {
sut.jumpAt(x: 10, y: 20)
expect(kittenMock.canJumpAtCall.called).to(beTruthy())
}
或者您需要检查方法是否被精确地调用了一定次数
it("should actually ask kitten to jump only once per call") {
sut.jumpAt(x: 18, y: 23)
expect(kittenMock.jumpCall.callsCount).to(equal(1))
sut.jumpAt(x: 80, y: 15)
expect(kittenMock.jumpCall.callsCount).to(equal(2))
}
所有方法调用都存储在模拟中,因此您可以轻松地检查模拟是否使用了正确的参数
it("should ask kitten if it's available to jump there with the same coords") {
sut.jumpAt(x: 10, y: 20)
expect(kittenMock.canJumpAtCall.capturedArgument?.x).to(equal(10))
expect(kittenMock.canJumpAtCall.capturedArgument?.y).to(equal(20))
}
ReactiveSwift存根支持
返回 SignalProducer
或 Action
方法的API与普通方法存根几乎相同。
设想RoboKitten协议略有变化,返回SignalProducer而不是纯值,这样我们可以关注电池电量
protocol RoboKitten {
func batteryStatus() -> SignalProducer<Int, NoError>
}
现在,您创建这个协议的模拟实现,但是您使用的是ReactiveCall
而不是FunctionCall
。唯一的不同之处在于我们添加了第三个类型约束来指定Error
。
class RoboKittenMock: RoboKitten {
let batteryStatusCall = ReactiveCall<(), Int, NoError>()
func batteryStatus() -> SignalProducer<Int, NoError> {
return stubCall(batteryStatusCall, argument:())
}
}
方法占位符
由于SignalProducer
受限于Value和Error类型,我们需要允许用户选择哪个来模拟方法调用。幸运的是,有方便的Result类型及其实现,它们与ReactiveSwift一起提供。因此,为了模拟ReactiveCall
,你使用Result
而不是之前使用FunctionCall
时的普通值。
// like this
kittenMock.batteryStatusCall.returns(.success(42))
// or
kittenMock.batteryStatusCall.returns(Result(value: 42))
// or in case you want this stub to return failure
kittenMock.batteryStatusCall.returns(.failure(ImagineThisIsError))
// or
kittenMock.batteryStatusCall.returns(Result(error: ImagineThisIsError))
其他一切都保持不变 :)
匹配器
SwiftyMock没有自己的匹配器,因此您可以使用最适合您的任何匹配器 :)
模板
您可以使用Sourcery自动生成模拟。
首先,创建Sourcery配置yml,并指定源文件、模板、生成的输出路径以及测试框架的测试路径。您可以看一下根目录下的.sourcery.yml
是如何配置的。
sources:
- ./Example/SwiftyMock/RoboKitten
templates:
- ./SwiftyMock/Templates
output:
path: ./Example/RoboKittenTests/Mocks/Generated
args:
testable: SwiftyMock_Example # here you specify your application module name, that you're importing for testing
其次,用// sourcery: Mock
注释标注您想为它们生成模拟的协议。
// sourcery: Mock
protocol RoboKitten {
// ...
}
第三,运行Sourcery命令sourcery --config .sourcery.yml --watch
,如果您想运行一个服务,每次您的源文件或模板发生变化时都会重新生成模拟。
或者,如果您只想生成一次模拟,则使用sourcery --config .sourcery.yml
。