由于 Swift 的高度静态特性,相比其他语言,Mock 和 Stub 更加困难。没有像 OCMock
或 Mockito
这样的动态 Mock 框架。通常的做法是手动编写 Mock 对象,如下所示:
protocol Foo {
func doSomething(arg: String) -> Int
}
class FooMock: Foo {
var doSomethingHasBeenCalled = false
var doSomethingArgument = String?
var doSomethingReturnValue: Int!
func doSomething(arg: String) -> Int {
doSomethingHasBeenCalled = true
doSomethingArgument = arg
return doSomethingReturnValue
}
}
这需要大量的样板代码(而且这只是一个简单的示例,例如,它不允许有条件的 Stub)。这就是 Mokka 的用武之地。
Mokka 提供了一个名为 FunctionMock<Args>
的测试辅助类(以及一个用于返回函数的变体,称为 ReturningFunctionMock<Args, ReturnValue>
),它负责处理:
"x"
调用,则此方法应返回 42
)有了这些辅助工具,定义 Mock 对象就方便多了
class FooMock: Foo {
let doSomethingFunc = ReturningFunctionMock<String, Int>()
func doSomething(arg: String) -> Int {
return doSomethingFunc.recordCallAndReturn(arg)
}
}
现在你可以使用该函数 Mock 对象进行验证
func testSomething() {
// ...
XCTAssertEqual(myMock.doSomethingFunc.callCount, 2)
XCTAssertEqual(myMock.doSomethingFunc.argument, "lorem ipsum")
// ...
}
并用于伪造返回值
func testSomething() {
// static return value
myMock.doSomethingFunc.returns(100)
// dynamic return value
myMock.doSomethingFunc.returns { $0 + 200 } // $0 is the argument(s) passed to the method
// conditional return value
myMock.doSomethingFunc.returns(123, when: { $0 == "foo" })
myMock.doSomethingFunc.returns(456, when: { $0 == "bar" })
myMock.doSomethingFunc.returns(789)
}
要通过 CocoaPods 安装 Mokka,只需将 Mokka
pod 添加到 Podfile 中的测试目标
pod 'Mokka'
您可以使用 Swift Package Manager 安装 Mokka。只需将此仓库作为依赖项添加到您的 Package.swift
文件(并且不要忘记也将 "Mokka"
作为测试目标中的依赖项添加)
dependencies: [
.package(url: "https://github.com/danielr/Mokka", from: "1.0.0")
// ...
]
要使用 Carthage 安装 Mokka,请将其添加到您的 Cartfile 中
github "danielr/Mokka"
然后将构建的 Mokka.framework
拖到您的项目中,并确保它已添加到单元测试目标,而不是主应用程序目标。 如果您看到类似 "The bundle xxx couldn’t be loaded because it is damaged or missing necessary resources." 的错误,那么您可能需要调整测试目标的 Runtime Search Paths。
目前有三种类型的 Mock 辅助工具可用:
FunctionMock
: 允许记录对函数的调用(调用计数和参数),以及选择性地 Stub 函数的行为。 将此用于具有 Void
返回值的函数。ReturningFunctionMock
: 提供与 FunctionMock
相同的功能,但增加了伪造返回值的能力。 将此用于具有非 void 返回值的函数。PropertyMock
: 允许为属性提供伪造的值,并记录是否已读取或设置属性。第一步是使用 Mokka 的辅助工具实现您的 Mock。 对于所有类型的 Mock,通用方法是相同的:为 Mock 声明一个属性,并在您的函数实现中使用该 Mock 对象来记录对该函数的调用。
对于下面的示例,我们假设我们要 Mock 以下协议
protocol Engine {
func turnOn()
func turnOff()
var isOn: Bool { get }
func setSpeed(to value: Float) // kilometers per hour
func setSpeed(to value: Float, in unit: UnitSpeed)
func currentSpeed(in unit: UnitSpeed) -> Double
}
对于不返回值的方法,请使用 FunctionMock<Args>
。 此类具有一个泛型参数,该参数定义了参数的类型。
对于没有参数的函数,应为 Void
class EngineMock: Engine {
let turnOnFunc = FunctionMock<Void>(name: "turnOn()")
func turnOn() {
turnOnFunc.recordCall()
}
// ...
}
注意:Mock 初始化程序中的 name
参数是可选的。 它纯粹是信息性的,可能对错误消息有用(例如,更好的断言错误消息)。 最好以标准的 Swift #selector 语法提供名称。
对于具有单个参数的函数,只需使用该参数的类型
class EngineMock: Engine {
let setSpeedFunc = FunctionMock<Float>(name: "setSpeed(to:)")
func setSpeed(to value: Float) {
setSpeedFunc.recordCall(value)
}
// ...
}
对于具有多个参数的函数,您需要使用元组来表示参数(因为 Swift 不(还?)支持可变泛型参数)。 尽管不必这样做,但最好命名元组元素,这样在测试代码中引用它们时会更清楚。
class EngineMock: Engine {
let setSpeedInUnitFunc = FunctionMock<(value: Float, unit: UnitSpeed)>(name: "setSpeed(to:unit:)")
func setSpeed(to value: Float, in unit: UnitSpeed) {
setSpeedInUnitFunc.recordCall((value: value, unit: unit))
}
// ...
}
对于具有返回值的函数,请使用 ReturningFunctionMock<Args, ReturnValue>
。 这与 FunctionMock
的工作方式非常相似,但是为返回值类型添加了第二个泛型参数。 它还提供了一个 recordCallAndReturn()
方法,而不是 recordCall()
方法。
class EngineMock: Engine {
let currentSpeedFunc = ReturningFunctionMock<UnitSpeed, Double>(name: "currentSpeed(in:)")
func currentSpeed(in unit: UnitSpeed) -> Double {
return currentSpeedFunc.recordCallAndReturn(unit)
}
// ...
}
对于返回方法的参数,适用的规则与非返回函数相同(请参见上文)。 例如
ReturningFunctionMock<Void, Bool>
Int
和 String?
的参数并返回 Double
的函数的 Mock 将被声明为 ReturningFunctionMock<(arg1: Int, arg2: String?), Double>
在许多情况下,只需在 Mock 中声明一个带有默认值的存储属性来实现 Mock 协议的属性需求就足够了。 但是,当您希望能够明确地跟踪是否已读取或写入属性时,Mokka 的 PropertyMock
会很有帮助。 它是属性类型的泛型,并且在 Mock 实现中的使用是不言自明的:不要使用存储属性,而是声明一个计算属性,并将 getter 和 setter(如果是可设置的属性)委托给 PropertyMock
对象的 get()
和 set(_:)
方法
class EngineMock: Engine {
let isOnProperty = PropertyMock<Bool>(name: "isOn")
var isOn: Bool {
get { return isOnProperty.get() }
set { isOnProperty.set(newValue) }
}
// ...
}
请注意,您无需为属性提供默认值(如果未赋值,则 get()
方法将失败并显示 preconditionFailure
)。
Mock 的常见用例是验证方法是否已被调用,有时特别是被调用了多少次。 为此,FunctionMock
和 ReturningFunctionMock
都提供了一些属性:
called: Bool
返回该方法是否已被调用(一次或多次)。calledOnce: Bool
返回该方法是否正好被调用一次。callCount: Int
该方法已被调用的次数。XCTAssertTrue(engineMock.setSpeedFunc.called)
XCTAssertTrue(engineMock.setSpeedFunc.calledOnce)
XCTAssertEqual(engineMock.setSpeedFunc.callCount, 3)
除了验证函数是否已被调用之外,您通常还想检查函数被调用的参数。 您可以通过 arguments
属性来做到这一点
XCTAssertEqual(engineMock.setSpeedInUnitFunc.arguments.value, 100.0)
XCTAssertEqual(engineMock.setSpeedInUnitFunc.arguments.unit, .kilometersPerHour)
(这要求您遵循命名元组成员的建议做法,请参见上文。 如果不这样做,则必须通过其索引访问参数,例如 arguments.0
。)
对于单参数函数(没有参数元组),您还可以使用 argument
属性,这看起来更好
XCTAssertEqual(engineMock.setSpeedFunc.argument, 100.0)
有时需要 Stub 函数的行为,例如引入一些重要的副作用。 一个常见的例子是调用委托方法。 您可以通过提供一个将在调用该函数时执行的闭包来做到这一点。 将为闭包提供函数参数
let delegate = FooDelegateMock()
someMock.myFunction.stub { arg in
delegate.somethingHappened(with: arg)
}
对于返回函数,能够伪造返回值至关重要。 Mokka 提供了 3 种方法来实现此目的:静态返回值、动态返回值和条件返回值。 让我们看一下每一个。
在大多数情况下,提供一个应由 Mock 实现返回的简单静态值就足够了
engineMock.currentSpeedFunc.returns(100.0)
有时,提供动态生成的返回值很方便,通常取决于函数的参数。 您可以通过提供一个闭包来做到这一点
engineMock.currentSpeedFunc.returns { unit in
// always return 100 km/h, converted to the requested target unit
let kmhValue = Measurement(value: 100, unit: UnitSpeed.kilometersPerHour)
return kmhValue.converted(to: unit).value
}
静态和动态返回值都可以有条件地提供
engineMock.currentSpeedFunc.returns(100.00, when: { $0 == .kilometersPerHour })
engineMock.currentSpeedFunc.returns(62.137, when: { $0 == .milesPerHour })
engineMock.currentSpeedFunc.returns(0) // otherwise
这是在测试代码中如何使用由 PropertyMock
支持的属性
someMock.fooProperty.value = 10 // use value to access the underlying property value
// do something
XCTAssertTrue(someMock.fooProperty.hasBeenRead)
XCTAssertFalse(someMock.fooProperty.hasBeenSet
您可以在 MokkaExample 中找到一个简单的示例项目。
它包括
Car
)Engine
和 Battery
)这是一个最小的例子,但应该足以让您开始了解 Mokka 的概念。
Mokka 由 Daniel Rinser 创建和维护,@danielrinser。
Mokka 在 MIT 许可证 下可用。