Scout

更简单、动态的 Swift Mock 工具。

Build Status codecov

为什么选择 Scout?

假设我们有一个依赖于 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 来实现的。

要求

安装

Swift Package

将此仓库添加到你的 package 的 dependencies 中,类似于此仓库的 示例项目清单

Carthage

有一个 workaround 用于使用 Carthage 集成 Swift 包。 TL;DR;

开始使用

我建议从 Scout.playground 开始,获得一个叙述式的交互指南。 有关代码示例和 API 文档,请参阅 用法

用法

Mock

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

Mock#get

返回一个 @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

Mock#call

返回一个 @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? 强制转换为预期返回类型。

Mock#expect

返回一个动态 DSL 对象,该对象配置对 mock.get.<var>mock.call.<func> 的调用行为。

期望 Var 获取

只需访问所需的 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 还是函数调用而异

Var 期望

ExpectVarDSL 用于公开 var 访问的期望 DSL。 to 方法只有一种形式,它接受由任何工厂函数返回的 Expectation 实例

函数期望

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

在测试方法的底部,如果你想断言其所有期望都已满足,则应在你的 mock 类上调用 verify()

func testCallsBazIfFooIsBaz() {
    mockExample.expect.foo.to(`return`("baz"))
    mockExample.expect.baz().toBeCalled()

    bazFunc(mockExample)

    mockExample.verify()
}

在此示例中,如果 bazFunc 未调用 mockExample.baz(),则测试将失败。

如果未使用 toAlways 添加的任何期望,则测试不会失败。

Mockable

一旦你设置了一个 Mockable 类,你就可以使用 Mock 的一些方法,这要归功于 协议扩展,它为了方便起见,在它嵌入的类上公开了 Mock 的一些方法。 这可以防止你不得不在所有测试中键入 .mock.expect...

注意事项

使用带有 Scout 的 mock 进行的测试应将 continueAfterFailure 设置为 false,否则由于展开意外的 nil 错误,测试可能会崩溃。

@dynamicCallable 有一些事情做不到