使用SwiftyMocky生成模拟并简化Swift中的单元测试

简单的5点指导可将您的单元测试带入一个全新的水平。

总体来说,Swift世界最近发生了很多事情,这种趋势并没有错过对开发的一部分进行测试的机会。 尽管良好的测试实践正在传播,但对旨在帮助该过程的工具的需求也在增长。 我很高兴成为一个团队的成员之一,该团队致力于开发一种名为SwiftyMocky的解决方案。

1.模拟是测试双

一般而言,在谈论单元测试和测试时,有一整套特殊目的的对象,称为“测试双打”。 在本文中,我将仅简要介绍它们,因为需要理解SwiftyMocky背后的整个概念。

Test Doubles背后的故事很简单。 由于缺乏更好的词汇,经典的测试方法通常依赖于状态验证。 我们为sut及其依赖项创建初始状态,然后验证执行测试后的状态是否符合我们的期望。 尽管如此,并不是在每个测试用例中我们都在测试sut ,我们可以使用实际的实现作为其依赖项。

数据库操作或网络调用就是一个很好的例子。 我们真的不想在这里进行真正的通话,因此sut尝试进行的了解通常足以确保有效性。 另一个很好的例子(来自Martin Fowler的帖子)是发送电子邮件-很难从测试角度进行验证。

这就是我们的特殊情况对象出现的地方。 基于Gerard Meszaros提出的词汇(我个人觉得非常有用),我将其分为以下几类:

  • 虚拟 :完全没有实现,该对象的整体唯一目的是满足依赖关系要求。
  • 伪造的 :简单但有效的实现,通常采用捷径。 例如,伪存储将宁愿使用某些内存数据结构(如数组或字典),而不是包装核心数据操作。 在提供与具体实现相同的功能的同时,它使状态验证更加容易。
  • 存根 :包含预定义的响应和答案,通常严格针对特定测试用例定义。
  • Spy :存根,不仅返回预定义的答案,还记录调用的方法。
  • 模拟 :预先设定的期望值,可以验证sut的行为是否符合预期。

在本文的其余部分,我们将重点介绍Mock ,因为它可以处理正确测试所需的大多数内容。

请注意,虽然上述单词在测试双精度类型之间严格区分,但是在大多数工具中, Mock 都是扩展的,具有 Stub Spy的 组合功能 SwiftyMocky中, 我们执行相同的操作,因此,每当引用 Mock时 ,我们的意思是同时提供 Stub Spy 功能的对象。

尽管拥有Mock对象的想法很明确,并且收益不能被夸大,但这给我们带来了一个问题。 必须编写模拟实现,如果有适当的层分离,则在大型项目中这可能会成为相当大的开销。

在具有适当反映的语言中,有许多库和框架可以在运行时创建Mock ,使您可以选择一种最适合您的测试风格的库。

2. SwiftyMocky-这个想法

由于Swift不支持反射(还?),因此需要其他方法。 有些解决方案部分是手动的(当您必须实现模拟手册的一部分时),还有一些使用元编程来生成完整的模拟实现的解决方案。

SwiftyMocky属于第二类(最初由PrzemysławWośko提出)。 整个概念基于Sourcery(由KrzysztofZabłocki编写),并采用了元编程概念。

第一步是扫描源,并检查可能是模拟对象的类型。 在SwiftyMocky中,可以通过注释或采用AutoMockable协议来完成。

第二步是生成swift文件,其中包含所有模拟的实现,并采用了各种Mock协议(例如Mock和StaticMock,仅举几例)。 可以将该文件添加到测试目标,从而允许使用所有生成的类。

在第三步中,我们可以使用整套方便的方法针对采用Mock协议的类编写测试,从而允许:

  • 存根方法响应(存根)
  • 间谍以检查被称为什么以及具有哪些属性(间谍)
  • 验证是否触发了特定方法,执行了多少次等(模拟)

值得注意的是,使用 SwiftyMocky, 您可以专注于第3步。只需编写测试,然后让库处理样板文件的其余部分即可。

3.为什么选择SwiftyMocky? 最佳功能概述

自动模拟生成:

这大大减少了设置测试所需的时间。 而且它更容易采用更改,因为可以随时随地更新模拟实现。

让我们考虑简单的协议。

//sourcery: AutoMockable 
protocol UserStorageType {
func surname(for name: String) -> String
func storeUser(name: String, surname: String)
}

它将导致类似:

 // MARK: - UserStorageType 
class UserStorageTypeMock: UserStorageType, Mock {
func surname(for name: String) -> String { // ... }
func storeUser(name: String, surname: String) { // ... }

// ... mock internals

请注意,目前仅协议是模拟生成的主题。 我们相信接口是在Swift中进行层分离和建立依赖关系的正确方法。 但是,将来可以使用模拟类(但并非没有限制)

容易存根:

对于我们的模拟实现而言,最重要的事情之一就是指定方法的返回值。 在典型的普通存根中,我们只是对值进行硬编码,但它不是很灵活。

SwiftyMocky提供了一种很好的方法来指定返回值,同时具有简单的语法和灵活性。 请从上面考虑模拟,该方法声明方法surname(for name: String) -> String

SwiftyMocky允许我们执行以下操作:

 let mock = UserStorageTypeMock() 
 // For all calls with name Johny, we should return Bravo 
Given(mock, .surname(for: .value("Johny"), willReturn: "Bravo"))
// For all other calls, regardless of value, we return Kowalsky Given(mock, .surname(for: .any, willReturn: "Kowalsky"))
 XCTAssertEqual(mock.surname(for: "Johny"), "Bravo") 
XCTAssertEqual(mock.surname(for: "Mathew"), "Kowalsky")
XCTAssertEqual(mock.surname(for: "Joanna"), "Kowalsky")

请注意. 在方法的开头,因为它严格地指:

自动完成:

我们利用尽可能多的自动完成功能来辅助编写测试的过程。 在执行给定,验证,模拟执行时,键入 . 获取适合特定情况的模拟协议声明的所有方法的列表 。 所有这些都是类型安全的。

在这种情况下,一张图像值得一千个单词:

我们不会强迫用户记住其他东西。 并且不再需要易于出错的“字符串”标识符(就像OCMock一样)。

容易监视:

存根通常是不够的,因此我们准备了一种方法来验证是否调用了一种方法(以及调用了多少次)。 语法与“给定”中提出的语法一致:

 // inject mock to sut. Every time sut saves user data, it should trigger storage storeUser method 
sut.usersStorage = mockStorage
 sut.saveUser(name: "Johny", surname: "Bravo") 
sut.saveUser(name: "Johny", surname: "Cage")
sut.saveUser(name: "Jon", surname: "Snow")
 // check if Jon Snow was stored at least one time Verify(mockStorage, .storeUser(name: .value("Jon"), surname: .value("Snow"))) 
// storeUser method should be triggered 3 times in total, regardless of attributes values
Verify(mockStorage, 3, .storeUser(name: .any, surname: .any))
// storeUser method should be triggered 2 times with name Johny
Verify(mockStorage, 2, .storeUser(name: .value("Johny"), surname: .any))

灵活性:

在任何有意义的地方,我们都将方法属性包装到Parameter枚举中。 它允许指定我们是否关心显式值:

  • 使用.any是通配符,因此我们可以不考虑属性而指定返回值,或验证是否完全调用了该方法。
  • 使用.value()我们可以指定显式值。 由于更明确的情况优先于较不明确的情况,因此我们可以为返回值指定特殊情况,或验证使用特定属性调用的方法的确切数目。

所有情况都可以混合在一起,从而为侦听和监视提供了很好的灵活性。

就在我写这篇文章的那一刻,2.0版即将面世,向Parameter添加.matching(Type-> Bool)大小写,提供了更大的灵活性。

泛型:

当我写这篇文章时,似乎SwiftyMocky是唯一支持泛型的Swift工具。 (或者至少是泛型和模拟生成)。

 //sourcery: AutoMockable 
protocol ProtocolWithGenericMethods {
func methodWithGeneric(lhs: T, rhs: T) -> Bool
}

我们可以在如下测试中使用它:

 let mock = ProtocolWithGenericMethodsMock() 
 // For generics - you have to use .any(ValueType.Type) to avoid ambiguity 
Given(mock, .methodWithGeneric(lhs: .any(Int.self), rhs: .any(Int.self), willReturn: false))
 Given(mock, .methodWithGeneric(lhs: .any(String.self), rhs: .any(String.self), willReturn: true)) 
 // In that case it is enough to specify type for only one element, so the type inference could do the rest 
Given(mock, .methodWithGeneric(lhs: .value(1), rhs: .any, willReturn: true))
 XCTAssertEqual(mock.methodWithGeneric(lhs: 1, rhs: 0), true) 
XCTAssertEqual(mock.methodWithGeneric(lhs: 0, rhs: 1), false)
XCTAssertEqual(mock.methodWithGeneric(lhs: "a", rhs: "b"), true)
 // Same applies to verify - specify type to avoid ambiguity 
Verify(mock, 2, .methodWithGeneric(lhs: .any(Int.self), rhs: .any(Int.self)))
Verify(mock, 1, .methodWithGeneric(lhs: .any(String.self), rhs: .any(String.self)))

4.安装和设置

CocoaPods和Carthage均提供SwiftyMocky,根据使用的依赖管理器,运行它所需的其他步骤有所不同。 完整说明可在此处获得。

最简单的方法是使用CocoaPods,因为它也可以处理Sourcery的情况(如果是迦太基,则必须自己动手做)。

在撰写本文时,Sourcery的版本为0.9.0,是为Swift 4.0.0编译的。 对于Xcode 9.1的用户来说,这通常会导致问题,因此在这种情况下,SwiftyMocky会附带脚本来覆盖Sourcery(请 在此处 检查

整个设置在yml配置文件中完成。 我们可以为源文件指定多个位置,无论是文件列表还是文件夹列表。 通常如下所示:

 sources: # locations to scan 
- ./ExampleApp
- ./ExampleAppTests
templates: # location of SwiftyMocky templates in Pods directory
- ./Pods/SwiftyMocky/Sources/Templates
output:
./ExampleApp # here Mock.generated.swift will be placed
args: # additional arguments
testable: # assure @testable imports added
- ExampleApp
import: # assure all external imports for mocks
- RxSwift
- RxBlocking
excludedSwiftLintRules: # for lint users
- force_cast
- function_body_length
- line_length
- vertical_whitespace

然后,为了触发生成,使用指定的配置调用Sourcery。 虽然通常看起来像这样:

Pods/Sourcery/bin/Sourcery.app/Contents/MacOS/Sourcery --config mocky.yml

它可以包装在Rakefile中,从而可以进行rake mock

 # Rakefile 
task :mock do
sh "Pods/Sourcery/bin/Sourcery.app/Contents/MacOS/Sourcery --config mocky.yml"
end

由于模拟生成需要一些时间,因此在将其添加到构建运行脚本阶段时看不到任何价值,但是我们发现使用Xcode行为和键绑定在项目中重新生成模拟相当方便。

5.总结

尽管有很多obj-c好的工具,但是Swift仍然缺少一些适当的解决方案。 SwiftyMocky试图解决此问题,提供自动模拟生成并易于获取和使用语法,并尽可能利用自动完成功能。

这仍然是新事物,加班时间变得越来越成熟,但是仍然有可能进行更大的改变。 这样做的好处是,它仍然需要增长速度,提供新功能和固定电流。

如果我必须说出为什么您应该尝试一下,我会说:

  • 设置非常简单
  • 好的文件
  • 简单易用的语法(利用自动完成功能)
  • 对泛型的支持