Cuckoo 的创建是由于缺少合适的 Swift 模拟框架。 我们构建的 DSL 与 Mockito 非常相似,因此任何来自 Java/Android 的人都可以立即上手使用它。
Cuckoo 有两个部分。 一个是运行时,另一个是一个 OS X 命令行工具,简称为 Cuckoonator。
不幸的是,Swift 没有合适的反射机制,因此我们决定使用编译时生成器来遍历您指定的文件,并生成支持结构/类,这些结构/类将在您的测试目标中被运行时使用。
生成的文件包含足够的信息,可以为您提供适当的控制能力。 它们基于继承和协议遵循工作。 这意味着只有可覆盖的内容才能被模拟。 由于 Swift 的复杂性,检查所有极端情况并不容易,因此如果您发现一些意想不到的行为,请提交 issue。
所有更改和新功能的列表可以在这里找到。
Cuckoo 是一个强大的模拟框架,支持
as TYPE
符号,并且可以通过显式指定类型来覆盖)由于上述限制,Cuckoo 不支持不可覆盖的代码结构。 这包括
struct
- 解决方法是使用一个公共协议final
或 private
修饰符的所有内容Cuckoo 在以下平台上工作
URL: https://github.com/Brightify/Cuckoo.git
警告:请务必仅将 Cuckoo 添加到测试目标。
一切准备就绪后,转到测试目标的 Build Phases,并将插件 CuckooPluginSingleFile
添加到 Run Build Tool Plug-ins。
Cuckoo 运行时可通过 CocoaPods 获得。 要安装它,只需在 Podfile 中的测试目标中添加以下行
pod 'Cuckoo', '~> 2.0'
并将以下 Run script
构建阶段添加到测试目标的 Build Phases
中,在 Compile Sources
阶段之上
# Skip for indexing
if [ $ACTION == "indexbuild" ]; then
exit 0
fi
# Skip for preview builds
if [ "${ENABLE_PREVIEWS}" = "YES" ]; then
exit 0
fi
"${PODS_ROOT}/Cuckoo/run"
运行一次后,找到 GeneratedMocks.swift
并将其拖到您的 Xcode 测试目标组中。
重要提示:为了让您的模拟之旅更轻松,请务必确保运行脚本位于 Compile Sources
阶段之上。
注意: 从 Xcode 15 开始,Build Settings 中的 flag ENABLE_USER_SCRIPT_SANDBOXING
默认是 Yes
。这意味着 Xcode 会沙箱化脚本,因此读取输入文件和写入输出文件将被禁止。因此,运行上面的脚本可能会无法访问文件。要防止 Xcode 沙箱化脚本,请将此选项更改为 No
。
也可以直接在 Run script
中的 Input Files
表单中指定输入文件。
注意:运行脚本中的所有路径都必须是绝对路径。变量 PROJECT_DIR
自动指向您的项目目录。请记住包含继承的类和协议的路径,以便模拟/存根父类和祖父母类。
在项目的根目录中,创建 Cuckoofile.toml
配置文件
# You can define a fallback output for all modules that don't define their own.
output = "Tests/Swift/Generated/GeneratedMocks.swift"
[modules.MyProject]
output = "Tests/Swift/Generated/GeneratedMocks+MyProject.swift"
# Standard imports added to the generated file(s).
imports = ["Foundation"]
# Public imports if needed due to imports being internal by default from Swift 6.
publicImports = ["ExampleModule"]
# @testable imports if needed.
testableImports = ["RxSwift"]
sources = [
"Tests/Swift/Source/*.swift",
]
exclude = ["ExcludedTestClass"]
# Optionally you can use a regular expression to filter only specific classes/protocols.
# regex = ""
[modules.MyProject.options]
# glob = false
# Docstrings are preserved by default, comments are omitted.
keepDocumentation = false
# enableInheritance = false
# protocolsOnly = true
# omitHeaders = true
# If specified, Cuckoo can also get sources for the module from an Xcode target.
[modules.MyProject.xcodeproj]
# Path to folder with .xcodeproj, omit this if it's at the same level as Cuckoofile.
path = "Generator"
target = "Cuckoonator"
# You can define as many modules as you need, each with different sources/options/output.
[modules.AnotherProject]
# ...
Cuckoo 的用法类似于 Mockito 和 Hamcrest。 但是,由于生成模拟和 Swift 语言本身的原因,存在一些差异和限制。 所有受支持的功能列表可以在下面找到。 您可以在 tests 中找到完整的示例。
可以使用与被模拟类型相同的构造函数来创建模拟。 模拟类的名称始终对应于带有 Mock
前缀的被模拟类/协议的名称(例如,协议 Greeter
的模拟称为 MockGreeter
)。
let mock = MockGreeter()
Spy 是一种特殊的 Mocks,默认情况下每个调用都会转发给 victim。 当您需要一个 spy 时,请给 Cuckoo 一个类,然后您就可以在模拟实例上调用 enableSuperclassSpy()
(或 withEnabledSuperclassSpy()
),它将表现得像父类的 spy。
let spy = MockGreeter().withEnabledSuperclassSpy()
可以通过将方法作为 when
函数的参数来完成存根。 存根调用必须在特殊的存根对象上完成。 您可以使用 stub
函数获取对它的引用。 此函数接受要存根的模拟的实例和一个闭包,您可以在其中进行存根。 此闭包的参数是存根对象。
注意:目前存根对象可以从闭包中逃逸。 您仍然可以使用它来存根调用,但在实践中不建议这样做,因为此行为将来可能会更改。
调用 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
返回包含所有捕获值的数组。
Cuckoo 使用匹配器将您的模拟连接到您的测试代码。
您可以模拟任何符合 Matchable
协议的对象。 标准的内置类型(如 Int
、String
)已经通过 Cuckoo 符合要求。 自动符合性合成适用于 Array
等。
如果 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),请参见此 issue。
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"))
你可以使用 CallMatcher
的实例作为 verify
函数的第二个参数。它的主要功能是断言调用发生的次数。但是 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
混合使用。
以下函数用于重置 mock 对象的桩和/或调用。
/// 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...)
桩用于抑制真实代码的执行。桩与 Mock 的不同之处在于,它们不支持桩的设置和验证。它们可以使用与 mock 类型相同的构造函数创建。桩类的名称始终与 mock 类/协议的名称相对应,并带有 Stub
后缀 (例如,协议 Greeter
的桩被称为 GreeterStub
)。
let stub = GreeterStub()
当在桩上调用方法或访问/设置属性时,不会发生任何事情。如果一个方法或属性需要返回值,DefaultValueRegistry
会提供一个默认值。可以使用桩来设置 mock 对象的隐式(无)行为,而无需使用 thenDoNothing()
,例如:MockGreeter().spy(on: GreeterStub())
。
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
这些选项仅用于下载或构建生成器,不会干扰生成的 mock 对象的结果。
当执行 run 脚本 且不带任何参数时,它会简单地搜索 cuckoonator
文件,如果该文件不存在,则从源代码构建它,然后在之后运行生成器。
要从 GitHub 下载生成器而不是构建它,请使用 --download
选项作为第一个参数 (例如,run --download
)。如果您在构建过程中遇到较长时间的问题(尤其是在 CI 中),这可能是解决问题的方法。
注意:如果在使用 --download
选项时遇到 Github API 速率限制,run 脚本 会引用环境变量 GITHUB_ACCESS_TOKEN
。 将此行(用您的 GitHub 令牌替换 Xs,无需其他权限)添加到 run
调用上方的脚本构建阶段
export GITHUB_ACCESS_TOKEN="XXXXXXX"
构建选项 --clean
强制构建或下载指定的版本,即使生成器已经存在。如果需要,运行脚本还会将生成器同步到正确的版本(通过从源代码构建或使用 --download
下载)。
我们建议仅在尝试修复编译问题时使用 --clean
,因为它会强制每次都构建(或下载),这会使测试时间比需要的长得多。
为了支持 Objective-C,Cuckoo 封装了 OCMock
并使用其经过实战检验的功能,为 Swift 带来对 Mock 各种 Objective-C 类和协议的支持。
例如
let tableView = UITableView()
// stubbing the class is very similar to stubbing with Cuckoo, note the `objcStub` method.
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
// An unfortunate drawback is that arguments will miss type information, so the closure needs to cast them manually.
let (tableView, indexPath) = (args[0] as! UITableView, args[1] as! IndexPath)
print(tableView, indexPath)
}
}
// Invoking is exactly the same as you'd expect.
XCTAssertEqual(mock.numberOfSections(in: tableView), 1)
mock.tableView(tableView, accessoryButtonTappedForRowWith: IndexPath(row: 14, section: 2))
// `objcVerify` replaces `verify` to test the interaction with methods/variables.
objcVerify(mock.numberOfSections(in: tableView))
objcVerify(mock.tableView(tableView, accessoryButtonTappedForRowWith: IndexPath(row: 14, section: 2)))
Cuckoo 测试中提供了详细的用法,以及此 Swift-ObjC 桥的 DO 和 DON'T。
Cuckoo 对所有人开放,我们希望您帮助我们制作最好的 Swift Mock 库。对于 Cuckoo 的开发,请按照以下步骤操作
make init
。这将安装必要的工具(包括 Tuist,如果尚未安装),生成项目,安装依赖项并在 Xcode 中打开它。make
。这将生成项目,安装依赖项并在 Xcode 中打开它。Cuckoo-iOS
、Cuckoo-tvOS
或 Cuckoo-macOS
的任何 scheme,并通过运行测试 (⌘+U) 进行验证。注意:每当您检出另一个分支时,请确保再次运行 make
,该项目利用 Tuist 进行项目生成。
该项目由两部分组成 - 运行时和代码生成器。在 Xcode 中打开 Cuckoo.xcworkspace
时,您会看到以下目录: - Source
- 运行时源文件 - Tests
- 运行时部分的测试 - Generator.xcodeproj
- 包含生成器源代码的项目(使用 Generator
scheme 运行它)
感谢您的帮助!
Cuckoo 在 MIT 许可证下可用。