Cuckoo
Mock您的Swift对象!
简介
Cuckoo是由于缺乏合适的Swift模拟框架而创建的。我们构建了类似Mockito的DSL,因此任何从Java/Android过来的人都可以立即上手并用它。
工作原理
Cuckoo由两部分组成。一部分是运行时,另一部分是一个简单的名为CuckooGenerator的OS X命令行工具。
不幸的是,Swift没有合适的反射,所以我们决定使用编译时生成器来遍历你指定的文件并生成运行时将在测试目标中使用的支持结构和类。
生成的文件包含足够的信息,可以让你获得正确数量的权限。它们基于继承和协议采用工作。这意味着只有可覆盖的事物才能被模拟。由于Swift的复杂性,检查所有边缘情况并不容易,所以如果你发现一些意外的行为,请提交问题反馈。
更新日志
所有更改和新功能列表请在此处查看:here。
功能特性
Cuckoo是一个强大的模拟框架,支持以下功能:
- 继承(祖父母方法)
- 泛型
- 实例变量的简单类型推断(与初始化器、
as TYPE
表示法和显式指定类型重叠时可以覆盖) - 使用OCMock的Objective-C模拟
不支持的功能
由于上述限制,Cuckoo不支持不可覆盖的代码结构。这包括
- 结构体(struct) - 解决方法是使用通用协议
- 具有
final
或private
修饰符的所有内容 - 全局常量和函数
- 静态属性和方法
Cuckoo运行在以下平台上
- iOS 11+
- MacOSX 10.13+
- tvOS 11+
watchOS由于缺少XCTest库,目前还不支持。
注意:版本1.2.0
是最后一个支持Swift 4.2
的版本。使用版本1.3.0
+以支持Swift 5
及以上。
Cuckoo运行时可以通过CocoaPods获取。要安装它,只需将以下行添加到您的Podfile中您的测试目标的测试目标
pod 'Cuckoo'
并将以下Run script
构建阶段添加到您的测试目标的Build Phases
中,在Compile Sources
阶段之上
if [ $ACTION == "indexbuild" ]; then
echo "Not running Cuckoo generator during indexing."
exit 0
fi
# Skip for preview builds
if [ "${ENABLE_PREVIEWS}" = "YES" ]; then
echo "Not running Cuckoo generator during preview builds."
exit 0
fi
# Define output file. Change "${PROJECT_DIR}/${PROJECT_NAME}Tests" to your test's root source folder, if it's not the default name.
OUTPUT_FILE="${PROJECT_DIR}/${PROJECT_NAME}Tests/GeneratedMocks.swift"
echo "Generated Mocks File = ${OUTPUT_FILE}"
# Define input directory. Change "${PROJECT_DIR}/${PROJECT_NAME}" to your project's root source folder, if it's not the default name.
INPUT_DIR="${PROJECT_DIR}/${PROJECT_NAME}"
echo "Mocks Input Directory = ${INPUT_DIR}"
# Generate mock files, include as many input files as you'd like to create mocks for.
"${PODS_ROOT}/Cuckoo/run" generate --testable "${PROJECT_NAME}" \
--output "${OUTPUT_FILE}" \
"${INPUT_DIR}/FileName1.swift" \
"${INPUT_DIR}/FileName2.swift" \
"${INPUT_DIR}/FileName3.swift"
# ... and so forth, the last line should never end with a backslash
# After running once, locate `GeneratedMocks.swift` and drag it into your Xcode test target group.
重要:为了使您的模拟之旅更简单,请确保运行脚本在Compile Sources
阶段之上。
注意:为了避免在 Xcode 并行构建阶段时发生竞态条件错误,请将 OUTPUT_FILE
的路径添加到构建阶段的“输出文件”部分。如果您发现即使添加了新更改,OUTPUT_FILE
仍未重新生成,那么在构建阶段的“输入文件”部分添加模拟文件可能会有所帮助。
输入文件也可以直接在 运行脚本
的 输入文件
形式下指定。
注意:运行脚本中所有路径都必须是绝对路径。变量 PROJECT_DIR
自动指向您的项目目录。请记住,为了对父类和祖父母类进行模拟,需要包括继承的类和协议的路径。
Swift Package Manager
- 在 Xcode 中,从菜单中导航:文件 > Swift Packages > 添加包依赖
- 添加
https://github.com/Brightify/Cuckoo.git
- 选择“到下一个主要版本”并输入
1.9.1
Cuckoo 依赖于一个当前无法使用 SPM 下载的脚本。但是为了方便,您可以将其复制到终端以下载最新的 run
脚本。如果未来的 run
脚本发生变化,您需要再次执行此命令。
curl -Lo run https://raw.githubusercontent.com/Brightify/Cuckoo/master/run && chmod +x run
设置完成后,使用与上述相同的 Run script
阶段,并将
"${PODS_ROOT}/Cuckoo/run"
替换为
"${PROJECT_DIR}/run" --download
--download
选项是必要的,因为 Generator
源不会克隆到您的项目中(它们在 DerivedData
中,无法访问)。您可以在其后添加版本(例如 1.9.1
)以获取特定版本的 cuckoo_generator
。如果您正在更改版本,请还使用 --clean
以替换当前的 cuckoo_generator
。
Carthage
要使用 Carthage 包含 Cuckoo,请将以下行添加到您的 Cartfile 中
github "Brightify/Cuckoo"
然后使用上面提到的 Run script
并替换
"${PODS_ROOT}/Cuckoo/run"
替换为
"Carthage/Checkouts/Cuckoo/run"
别忘了将框架添加到您的项目中。
2. 使用方法
Cuckoo 的使用与 Mockito 和 Hamcrest 类似。然而,由于 Cuckoo 是通过生成模拟器和 Swift 语言本身的限制,因此存在一些差异和限制。以下是所有支持的功能列表。你可以在 测试 中找到完整的示例。
Mock 初始化
Mock 可以使用与被模拟类型相同的构造函数创建。模拟类的名称始终与被模拟类/协议的名称相对应,并在前面加上 Mock
前缀(例如,协议 Greeter
的模拟称为 MockGreeter
)。
let mock = MockGreeter()
Spy
Spies 是一种特殊的 Mock,其中每个调用默认情况下都会转发给受害者。当需要 spy 时,给 Cuckoo 一个类,然后你可以在模拟实例上调用 enableSuperclassSpy()
(或 withEnabledSuperclassSpy()
),它将充当父类的 spy。
let spy = MockGreeter().withEnabledSuperclassSpy()
Stubbing
可以使用将方法作为when
函数的参数的方式来进行置换。置换调用必须在对特殊置换对象的调用上完成。您可以使用stub
函数获取对它的引用。此函数需要一个将要置换的mock实例以及一个闭包,在其中您可以进行置换。此闭包的参数是置换对象。
: 注意:目前置换对象可能从闭包中逃逸。您仍然可以使用它来进行置换调用,但实际应用中不推荐这样做,因为这可能在将来更改其行为。
在调用when
函数之后,您可以使用以下方法指定下一步要做什么
/// Invokes `implementation` when invoked.
then(_ implementation: IN throws -> OUT)
/// Returns `output` when invoked.
thenReturn(_ output: OUT, _ outputs: OUT...)
/// Throws `error` when invoked.
thenThrow(_ error: ErrorType, _ errors: Error...)
/// Invokes real implementation when invoked.
thenCallRealImplementation()
/// Does nothing when invoked.
thenDoNothing()
可用的方法取决于被置换的方法特征。例如,对于不是抛出或重新抛出的方法,就无法使用thenThrow
方法。
方法置换的一个例子看起来是这样的
stub(mock) { stub in
when(stub.greetWithMessage("Hello world")).then { message in
print(message)
}
}
对于属性
stub(mock) { stub in
when(stub.readWriteProperty.get).thenReturn(10)
when(stub.readWriteProperty.set(anyInt())).then {
print($0)
}
}
请注意,get
和set
将在稍后进行验证时使用。
启用默认实现
除了置换之外,您还可以使用被模拟的原有类的实例来启用默认实现。每个未置换的方法/属性将根据原始实现来表现。
通过简单地调用提供的方法来启用默认实现
let original = OriginalClass<Int>(value: 12)
mock.enableDefaultImplementation(original)
对于传递类到方法中,无论是模拟类还是协议,都没有区别。但是,如果你使用struct
来符合我们要模拟的原协议,则会有区别。
let original = ConformingStruct<String>(value: "Hello, Cuckoo!")
mock.enableDefaultImplementation(original)
// or if you need to track changes:
mock.enableDefaultImplementation(mutating: &original)
: 注意,这仅涉及struct
。enableDefaultImplementation(_:)
和enableDefaultImplementation(mutating:)
在状态追踪上不同。
标准非可变方法enableDefaultImplementation(_:)
为默认实现创建struct
的副本,并与该副本一起工作。然而,可变方法enableDefaultImplementation(mutating:)
使用对struct
的引用,即使启用默认实现后,original
的变化也会反映在默认实现调用中。
除非您需要在代码内部保持一致性以跟踪更改,否则我们建议使用非可变方法来启用默认实现。
链式置换
可以实现链式置换。这在需要按顺序定义多个调用的不同行为时很有用。最后一个行为将用于之后的每个调用。语法如下
when(stub.readWriteProperty.get).thenReturn(10).thenReturn(20)
这相当于
when(stub.readWriteProperty.get).thenReturn(10, 20)
对 readWriteProperty
的第一次调用将返回 10
,之后的调用都将返回 20
。
您可以按需组合模拟方法。
模拟覆盖
在查找模拟匹配时,Cuckoo 将最高优先级赋予 when
的最后调用。这意味着多次以相同的函数和匹配器调用 when
将会覆盖以前的调用。另外,在指定具体参数匹配器之前,必须使用更一般的参数匹配器。
when(stub.countCharacters(anyString())).thenReturn(10)
when(stub.countCharacters("a")).thenReturn(1)
在此示例中,使用 a
调用 countCharacters
将返回 1
。如果您调换了模拟的顺序,输出将始终为 10
。
实际代码中的应用
执行前面的步骤后,可以调用模拟方法。将此模拟注入生产代码由您决定。
注意:调用未模拟的模拟将引发错误。在 spy 的情况下,将执行真实代码。
验证
对于验证调用,有函数 verify
。其第一个参数是模拟对象,第二个参数(可选)是调用匹配器。然后是调用及其参数。
verify(mock).greetWithMessage("Hello world")
属性的验证与它们的模拟相似。
您可以使用函数 verifyNoMoreInteractions
检查模拟上没有更多交互。
在 Swift 的泛型类型中,可以使用通用参数作为返回类型。为了正确验证这些方法,您需要能够指定返回类型。
// Given:
func genericReturn<T: Codable>(for: String) -> T? { ... }
// Verify
verify(mock).genericReturn(for: any()).with(returnType: String?.self)
参数捕获
您可以使用ArgumentCaptor
来捕获调用验证中的参数(在存根中这样操作不建议)。下面是一个示例代码
mock.readWriteProperty = 10
mock.readWriteProperty = 20
mock.readWriteProperty = 30
let argumentCaptor = ArgumentCaptor<Int>()
verify(mock, times(3)).readWriteProperty.set(argumentCaptor.capture())
argumentCaptor.value // Returns 30
argumentCaptor.allValues // Returns [10, 20, 30]
如你所见,方法capture()
用于创建调用匹配器,然后您可以通过属性value
和allValues
获取参数。value
返回最后捕获的参数,如果没有则返回nil。allValues
返回一个包含所有捕获值的数组。
3. 匹配器
Cuckoo使用匹配器将存根代码与待测试代码连接起来。
A) 对已知类型的自动匹配器
您可以模拟任何遵循Matchable
协议的对象。这些基本值扩展以遵循Matchable
Bool
String
Float
Double
Character
Int
Int8
Int16
Int32
Int64
UInt
UInt8
UInt16
UInt32
UInt64
当元素的类型符合Matchable
时,会自动生成针对Array
、Dictionary
和Set
的匹配器。
B) 自定义匹配器
如果Cuckoo不知道您正在尝试比较的类型,您必须使用ParameterMatcher
编写自己的方法equal(to:)
。将此方法添加到您的测试文件中
func equal(to value: YourCustomType) -> ParameterMatcher<YourCustomType> {
return ParameterMatcher { tested in
// Implementation of equality test for your custom type.
}
}
Matchable
类型的对象可能会导致
Command failed due to signal: Segmentation fault: 11
有关详细信息或示例(使用Alamofire),请参阅此问题。
参数匹配器
ParameterMatcher
自己也符合Matchable
协议。您可以创建自己的ParameterMatcher
实例,或者如果您想直接使用自定义类型,则可以使用Matchable
协议。可以通过以下函数获取标准ParameterMatcher
的实例:
/// Returns an equality matcher.
equal<T: Equatable>(to value: T)
/// Returns an identity matcher.
equal<T: AnyObject>(to value: T)
/// Returns a matcher using the supplied function.
equal<T>(to value: T, equalWhen equalityFunction: (T, T) -> Bool)
/// Returns a matcher matching any Int value.
anyInt()
/// Returns a matcher matching any String value.
anyString()
/// Returns a matcher matching any T value or nil.
any<T>(type: T.Type = T.self)
/// Returns a matcher matching any closure.
anyClosure()
/// Returns a matcher matching any throwing closure.
anyThrowingClosure()
/// Returns a matcher matching any non nil value.
notNil()
Cuckoo还提供了大量的方便的匹配器,用于处理数组和字典,允许您检查一个数组是否是某个数组的超集,是否包含其至少一个元素,或者与它完全不相关。
Matchable
可以使用方法or
和and
进行连接
verify(mock).greetWithMessage("Hello world".or("Hallo Welt"))
调用匹配器
作为verify
函数的第二个参数,您可以使用CallMatcher
的实例。它的主要功能是断言调用的次数。但是,matches
函数有一个类型为[StubCall]
的参数,这意味着您可以使用自定义的CallMatcher
来检查存根调用或产生某些副作用。
注意:调用匹配器在参数匹配器之后应用。因此,您将只能获得具有正确参数的所需方法存根调用。
标准调用匹配器是
/// Returns a matcher ensuring a call was made `count` times.
times(_ count: Int)
/// Returns a matcher ensuring no call was made.
never()
/// Returns a matcher ensuring at least one call was made.
atLeastOnce()
/// Returns a matcher ensuring call was made at least `count` times.
atLeast(_ count: Int)
/// Returns a matcher ensuring call was made at most `count` times.
atMost(_ count: Int)
与Matchable
一样,您可以使用方法or
和and
连接CallMatcher
。但是,您不能将Matchable
和CallMatcher
混合使用。
重置模拟
以下函数用于重置模拟上的存根和/或调用。
/// Clears all invocations and stubs of given mocks.
reset<M: Mock>(_ mocks: M...)
/// Clears all stubs of given mocks.
clearStubs<M: Mock>(_ mocks: M...)
/// Clears all invocations of given mocks.
clearInvocations<M: Mock>(_ mocks: M...)
伪造对象
伪造对象用于屏蔽实际代码。伪造对象与模拟对象不同,它们不支持伪造和验证。可以使用与模拟类型相同的构造函数创建。伪造类名始终对应于被模拟类/协议的名称,并在其后添加Stub
后缀(例如,协议Greeter
的伪造对象称为GreeterStub
)。
let stub = GreeterStub()
当在伪造对象上调用方法或访问/设置属性时,不会发生任何操作。如果要从方法或属性返回值,则由DefaultValueRegistry
提供默认值。可以使用伪造对象来为模拟设置隐式(无)行为,而无需使用thenDoNothing()
,如:MockGreeter().spy(on: GreeterStub())
。
DefaultValueRegistry
DefaultValueRegistry
用于伪造对象以获取返回类型的默认值。它只知道默认Swift类型(集合、数组和字典)、可选值和元组(最多6个值)。可以通过扩展添加更多值的元组。在使用前必须通过DefaultValueRegistry.register<T>(value: T, forType: T.Type)
注册自定义类型。此外,Cuckoo设置的默认值也可以通过此方法覆盖。如果其泛型类型已被注册,则不必注册集合、数组等。
DefaultValueRegistry.reset()
将登记表恢复到在register
方法进行任何更改之前的状态。
类型推断
Cuckoo对所有变量进行简单类型推断,这允许您的源代码更加简洁。类型推断试图从变量中提取类型名称的方式总共有3种
// From the explicitly declared type:
let constant1: MyType
// From the initial value:
let constant2 = MyType(...)
// From the explicitly specified type `as MyType`:
let constant3 = anything as MyType
寒鸦生成器
安装
对于正常使用,您可以跳过此过程,因为运行脚本会自动下载或构建正确的生成器版本。
自定义
您选择了更复杂的方法。您可以克隆此代码库并自行构建。查看运行脚本以获取更多灵感。
用法
生成器可以通过终端手动执行。每次调用包括构建选项、命令、生成器选项和参数。选项和参数取决于使用的命令。选项可以有附加参数。所有这些的名字均为大小写敏感。顺序如下
cuckoo build_options command generator_options arguments
构建选项
这些选项仅用于下载或构建生成器,不会干扰生成的模拟结果。
如果没有给出任何构建选项(只有指定在命令 command
之前才是有效的)直接运行 run 脚本,脚本将查找 cuckoo_generator
文件,如果不存在则从源代码构建它。
如果要从 GitHub 下载生成器而不是构建,请将 --download [version]
选项作为第一个参数使用(例如,run --download generate ...
或 run --download 1.5.0 generate ...
以获取特定版本)。如果您遇到了较长的构建时间问题(尤其是在 CI 中),这可能是一种解决方法。
注意:如果您在使用 --download
选项时遇到 GitHub API 速率限制,run 脚本 将引用环境变量 GITHUB_ACCESS_TOKEN
。请在上面的脚本构建阶段中 run
调用之前添加此行(将 Xs 替换为您的 GitHub 令牌,不需要额外的权限)
export GITHUB_ACCESS_TOKEN="XXXXXXX"
构建选项 --clean
强制构建或下载指定的版本,即使生成器已经存在。目前 run 脚本 不强制生成器的版本与 Cuckoo 版本相同。我们建议在更新 Cuckoo 或在生成的模拟代码中出现神秘的编译错误后使用此选项。请在报告关于不正确生成的模拟代码的问题之前,首先尝试使用此选项验证您的生成器是否过时。
我们只在尝试修复编译问题时建议使用 --clean
,因为它每次都会强制构建(或下载),这会使测试时间比必要的时间长。
生成器命令
generate
命令
生成模拟文件。
此命令接受可以用于调整生成器行为的选项,以下列出。
选项后面跟的是参数,在此例中是一个列表(由空格分隔),列出了您希望生成模拟的或为正确继承模拟所需的文件。
--output
(string)
生成模拟将保存的绝对路径。
如果提供的是目录的路径,则每个输入文件都将映射到具有模拟的单独输出文件。
如果提供的是文件的路径,则所有模拟都将在单个文件中生成。
默认值是 GeneratedMocks.swift
。
--testable
(string)[,(string)...]
逗号分隔的列表,包含要作为 @testable 导入的框架。
--exclude
(string)[,(string)...]
逗号分隔的列表,包含在模拟生成期间应跳过的类和协议。
--no-header
不生成文件头。
–no-timestamp
不要生成时间戳。
–no-inheritance
不要对父代和祖父代进行模拟/存根处理。
–file-prefix
(字符串)
目录中生成文件的名称将以此前缀开始。仅在输出路径是目录时有效。
–no-class-mocking
不要为类生成模拟。
–regex
(字符串)
用于匹配类和协议的正则表达式模式。所有不匹配的都被排除。可以与--exclude
一起使用,此时--exclude
具有更高优先级。
-g
或--glob
激活对指定的输入路径进行glob
解析。
-d
或--debug
以调试模式运行生成器。输出更多详细信息,包括生成的模拟内容(例如方法参数信息)。
version
命令
打印此生成器的版本。
help
命令
显示通用或特定命令的帮助信息。
在help
命令后,您可以指定另一个命令的名称,以显示特定命令的信息。
Objective-C 支持情况
Cuckoo 子规范 Cuckoo/OCMock
提供了针对 Objective-C 类和协议的模拟支持。
示例用法
let tableView = UITableView()
// stubbing the class is very similar to stubbing with Cuckoo
let mock = objcStub(for: UITableViewController.self) { stubber, mock in
stubber.when(mock.numberOfSections(in: tableView)).thenReturn(1)
stubber.when(mock.tableView(tableView, accessoryButtonTappedForRowWith: IndexPath(row: 14, section: 2))).then { args in
// `args` is [Any] of the arguments passed and the closure needs to cast them manually
let (tableView, indexPath) = (args[0] as! UITableView, args[1] as! IndexPath)
print(tableView, indexPath)
}
}
// calling stays the same
XCTAssertEqual(mock.numberOfSections(in: tableView), 1)
mock.tableView(tableView, accessoryButtonTappedForRowWith: IndexPath(row: 14, section: 2))
// `objcVerify` is used to verify the interaction with the methods/variables
objcVerify(mock.numberOfSections(in: tableView))
objcVerify(mock.tableView(tableView, accessoryButtonTappedForRowWith: IndexPath(row: 14, section: 2)))
具体用法可在 Cuckoo 测试中找到,包括 Swift-ObjC 之间的桥梁的 DOs(推荐做法)和 DON'Ts(不推荐做法)。
目前仅支持 CocoaPods。要安装,只需将以下行添加到您的 Podfile
文件中:
pod 'Cuckoo/OCMock'
贡献
Cuckoo 向所有人开放,我们希望您能帮助我们创建最佳的 Swift 模拟库。要进行 Cuckoo 开发,请遵循以下步骤:
- 请确保您已安装最新稳定的 Xcode 版本。
- 克隆 Cuckoo 仓库。
- 在终端中,在克隆的 Cuckoo 仓库根目录下运行
make
命令,这将生成项目,安装依赖项,并在 Xcode 中打开项目。 - 选择
Cuckoo-iOS
、Cuckoo-tvOS
或Cuckoo-macOS
中的任何方案(OCMock 方案包含Cuckoo_OCMock
)并运行测试以验证(⌘+U)。 - 浏览代码或提交更改的 pull request。
- 每次检出其他分支时,务必重新运行
make
命令。
项目由两部分组成 - 运行时和代码生成器。在 Xcode 中打开 Cuckoo.xcworkspace
文件时,您将看到以下目录:
感谢您的帮助!
灵感来源
已用库
许可证
Cuckoo 在 MIT 许可证 下提供。