更简单、动态的 Swift Mock 工具。
假设我们有一个依赖于 Example
协议的 TestSubject
。
protocol Example {
var foo: String { get }
func baz()
}
class TestSubject {
let example: Example
init(example: Example) {
self.example = example
}
func doAThing() {
if example.foo == "baz" {
example.baz()
}
}
}
如果你使用 Swift 进行过单元测试,你可能对以下模式非常熟悉
class ManualMockExample: Example {
var foo: String
var bazWasCalled: Bool = false
init(foo: String) {
self.foo = foo
}
func baz() {
bazWasCalled = true
}
}
class ManualMockExampleTests : XCTestCase {
func testBoilerplateIsTedious() {
let manualMock = ManualMockExample(foo: "baz")
let testSubject = TestSubject(example: manualMock)
testSubject.doAThing()
XCTAssertTrue(manualMock.bazWasCalled)
}
}
这些 <func>WasCalled
标记和 var
存根很可能在你编写的每个 mock 中都会重复。 如果你需要抛出异常、调用完成 block 或任何更复杂的操作,你的 mock 会变得更加复杂。 更糟糕的是,所有这些“mock 功能”都不容易在测试中重用。
Scout 旨在删除所有这些样板代码,从而使测试更易于阅读和编写。 这是通过使用声明式、函数式和动态 API 来创建和配置 mock 来实现的。
将此仓库添加到你的 package 的 dependencies
中,类似于此仓库的 示例项目清单。
有一个 workaround 用于使用 Carthage 集成 Swift 包。 TL;DR;
cd Carthage/Checkouts/Scout && swift package generate-xcodeproj
我建议从 Scout.playground
开始,获得一个叙述式的交互指南。 有关代码示例和 API 文档,请参阅 用法。
Mock
是所有其他 API 的入口点。 它旨在嵌入到符合协议的 mock 类中,如下所示
protocol Example {
var foo: String { get }
func baz()
}
class MockExample : Example, Mockable {
let mock = Mock()
var foo: String {
get {
return mock.get.foo
}
}
func baz() {
try! mock.call.baz()
}
}
此示例演示了要在 mock 类中使用的两个 API
返回一个 @dynamicMemberLookup
代理,该代理检索被访问的 var
的下一个期望值。 例如,要获取 var foo
的下一个期望值
protocol GetExample {
var foo: String
}
class MockGetExample : GetExample, Mockable {
let mock = Mock()
var foo: String {
get {
return mock.get.foo
}
}
}
动态成员代理是泛型的,这意味着它使用类型推断来确定 foo
应该是 String
。
返回一个 @dynamicCallable
代理,该代理将检索被调用函数的期望值。
protocol CallExample {
func baz(buz: Int) -> Int
}
class MockCallExample : CallExample, Mockable {
let mock = Mock()
func baz(buz: Int) {
return try! mock.call.baz(buz: buz) as! Int
}
}
确保将所有参数从包装类传递给 Mock
。
由于
call
被声明为throws
(以支持期望一个错误),如果它在未声明为throws
的函数中使用,则需要添加try!
。
截至 Swift 5,
call
不能是泛型的。 在 Swift 支持泛型@dynamicCallable
类型之前,你需要从Any?
强制转换为预期返回类型。
返回一个动态 DSL 对象,该对象配置对 mock.get.<var>
和 mock.call.<func>
的调用行为。
只需访问所需的 var
,然后使用所需的期望值调用 to
mockGetExample.expect.foo.to(`return`("baz"))
mockGetExample.foo // returns "baz"
如果在调用 foo
时没有任何期望值,则 Mock
将使测试失败。 如果在调用 verify()
时仍然存在期望值,则 Mock
将使测试失败。
与 var
期望值类似,调用所需的函数,然后使用所需的期望值作为参数调用 .to()
mockCallExample.expect.baz(buz: equalTo(3)).to(`return`(4))
mockCallExample.baz(3) // returns 4
如果使用除 3
以外的其他值调用 baz
,则 Mock
会使测试失败。 此外,如果在未期望任何调用时调用 baz
,则 Mock
将使测试失败。 如上所示,在期望调用带有参数的函数时,你需要指定一个 ArgMatcher
。
有关可用参数匹配器的列表,请参见 ArgMatcher
。 两个最简单的例子是
equalTo(value)
:检查参数是否等于指定的值。
any()
:接受任何参数。
如果参数未能满足指定的匹配器,则 Mock
将使测试失败。
一旦你在 expect
上检索了一个 var
或调用了一个函数,你需要使用 to()
方法设置一个期望值
mockCallExample.expect.baz(buz: equalTo(3)).to(`return`(4))
在这种情况下,期望是“返回 4”。 你可以期望的内容因你期望的是 var
还是函数调用而异
ExpectVarDSL
用于公开 var
访问的期望 DSL。 to
方法只有一种形式,它接受由任何工厂函数返回的 Expectation
实例
return
:一次或多次返回单个值(对于不喜欢反引号的人,别名为 returnValue
)。alwaysReturn
:类似于 return
,但总是这样。get
:从闭包中返回值。ExpectFuncDSL
为函数调用提供期望设置 DSL。 它有两种不同的签名
var
期望 DSL
mockExample.to(`return`("foo"))
另一种接受特定于函数的 exepctation 的 DSL,这些 exepctation 只是具有 FuncExpectationBlock
签名的函数。 你可以编写自己的
func incrementBy(_ amount: Int) -> FuncExpectationBlock {
return { (args: KeyValuePairs<String, Any?>) in
return args.first as! Int + amount
}
}
mockExample.expect.foo.to(incrementBy(1))
当你有更高级的行为需要期望时,这尤其方便,例如 调用完成 block。
还有一个 throw
期望,当你想让你的 mock 函数抛出一个错误时使用
mockThrowExample.expect.someThrowingFunc.to(`throw`(SomeError())
一旦你设置了 mock 的期望值,你需要验证它们是否满足。
在测试方法的底部,如果你想断言其所有期望都已满足,则应在你的 mock 类上调用 verify()
。
func testCallsBazIfFooIsBaz() {
mockExample.expect.foo.to(`return`("baz"))
mockExample.expect.baz().toBeCalled()
bazFunc(mockExample)
mockExample.verify()
}
在此示例中,如果 bazFunc
未调用 mockExample.baz()
,则测试将失败。
如果未使用
toAlways
添加的任何期望,则测试不会失败。
一旦你设置了一个 Mockable
类,你就可以使用 Mock
的一些方法,这要归功于 协议扩展,它为了方便起见,在它嵌入的类上公开了 Mock
的一些方法。 这可以防止你不得不在所有测试中键入 .mock.expect...
。
使用带有 Scout 的 mock 进行的测试应将 continueAfterFailure
设置为 false
,否则由于展开意外的 nil
错误,测试可能会崩溃。
@dynamicCallable
有一些事情做不到
Any
并强制转换。inout
参数。 解决方法是使用 inout
声明你的 mock 类,并在 mock 上进行非 inout
调用(有关示例,请参见 ExampleProject 测试)。