Cuckoo
Boilerplate-free mocking framework for Swift!
Install / Use
/learn @Brightify/CuckooREADME
Cuckoo
Mock your Swift objects!
Introduction
Cuckoo was created due to lack of a proper Swift mocking framework. We built the DSL to be very similar to Mockito, so anyone coming from Java/Android can immediately pick it up and use it.
How does it work
Cuckoo has two parts. One is the runtime and the other one is an OS X command-line tool simply called Cuckoonator.
Unfortunately Swift does not have a proper reflection, so we decided to use a compile-time generator to go through files you specify and generate supporting structs/classes that will be used by the runtime in your test target.
The generated files contain enough information to give you the right amount of power. They work based on inheritance and protocol adoption. This means that only overridable things can be mocked. Due to the complexity of Swift it is not easy to check for all edge cases so if you find some unexpected behavior, please file an issue.
Changelog
List of all changes and new features can be found here.
Features
Cuckoo is a powerful mocking framework that supports:
- [x] inheritance (grandparent methods)
- [x] generics
- [x] simple type inference for instance variables (works with initializers,
as TYPEnotation, and can be overridden by specifying type explicitly) - [x] Objective-C mocks utilizing OCMock
What will not be supported
Due to the limitations mentioned above, unoverridable code structures are not supportable by Cuckoo. This includes:
struct- workaround is to use a common protocol- everything with
finalorprivatemodifier - global constants and functions
- static properties and methods
Requirements
Cuckoo works on the following platforms:
- iOS 13+
- macOS 10.15+
- tvOS 13+
- watchOS 8+
Cuckoo
1. Installation
Swift Package Manager
URL: https://github.com/Brightify/Cuckoo.git
WARNING: Make sure to add Cuckoo to test targets only.
When you're all set, go to your test target's Build Phases and add plug-in CuckooPluginSingleFile to the Run Build Tool Plug-ins.
CocoaPods
Cuckoo runtime is available through CocoaPods. To install it, simply add the following line to your test target in your Podfile:
pod 'Cuckoo', '~> 2.0'
And add the following Run script build phase to your test target's Build Phases above the Compile Sources phase:
# 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"
After running once, locate GeneratedMocks.swift and drag it into your Xcode test target group.
IMPORTANT: To make your mocking journey easier, make absolutely sure that the run script is above the Compile Sources phase.
NOTE: From Xcode 15 the flag ENABLE_USER_SCRIPT_SANDBOXING in Build Settings is Yes by default. That means Xcode will sandbox the script so reading input files and writing output file will be forbidden. As a result running above script may fail to access the files. To prevent Xcode from sandboxing the script, change this option to No.
Input files can be also specified directly in Run script in Input Files form.
Note: All paths in the Run script must be absolute. Variable PROJECT_DIR automatically points to your project directory.
Remember to include paths to inherited Classes and Protocols for mocking/stubbing parent and grandparents.
2. Cuckoofile customization
At the root of your project, create Cuckoofile.toml configuration file:
# 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]
# ...
3. Usage
Usage of Cuckoo is similar to Mockito and Hamcrest. However, there are some differences and limitations caused by generating the mocks and Swift language itself. List of all the supported features can be found below. You can find complete examples in tests.
Mock initialization
Mocks can be created with the same constructors as the mocked type. Name of mock class always corresponds to the name of the mocked class/protocol with Mock prefix (e.g. mock of protocol Greeter is called MockGreeter).
let mock = MockGreeter()
Spy
Spies are a special type of Mocks where each call is forwarded to the victim by default. When you need a spy, give Cuckoo a class, then you'll then be able to call enableSuperclassSpy() (or withEnabledSuperclassSpy()) on a mock instance and it will behave like a spy for the parent class.
let spy = MockGreeter().withEnabledSuperclassSpy()
Stubbing
Stubbing can be done by calling methods as a parameter of the when function. The stub call must be done on special stubbing object. You can get a reference to it with the stub function. This function takes an instance of the mock that you want to stub and a closure in which you can do the stubbing. The parameter of this closure is the stubbing object.
Note: It is currently possible for the subbing object to escape from the closure. You can still use it to stub calls but it is not recommended in practice as the behavior of this may change in the future.
After calling the when function you can specify what to do next with following methods:
/// 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()
The available methods depend on the stubbed method characteristics. For example, the thenThrow method isn't available for a method that isn't throwing or rethrowing.
An example of stubbing a method looks like this:
stub(mock) { stub in
when(stub.greetWithMessage("Hello world")).then { message in
print(message)
}
}
As for a property:
stub(mock) { stub in
when(stub.readWriteProperty.get).thenReturn(10)
when(stub.readWriteProperty.set(anyInt())).then {
print($0)
}
}
Notice the get and set, these will be used in verification later.
Enabling default implementation
In addition to stubbing, you can enable default implementation using an instance of the original class that's being mocked. Every method/property that is not stubbed will behave according to the original implementation.
Enabling the default implementation is achieved by simply calling the provided method:
let original = OriginalClass<Int>(value: 12)
mock.enableDefaultImplementation(original)
For passing classes into the method, nothing changes whether you're mocking a class or a protocol. However, there is a difference if you're using a struct to conform to the original protocol we are mocking:
let original = ConformingStruct<String>(value: "Hello, Cuckoo!")
mock.enableDefaultImplementation(original)
// or if you need to track changes:
mock.enableDefaultImplementation(mutating: &original)
Note that this only concerns structs. enableDefaultImplementation(_:) and enableDefaultImplementation(mutating:) are different in state tracking.
The standard non-mutating method enableDefaultImplementation(_:) creates a copy of the struct for default implementation and works with that. However, the mutating method enableDefaultImplementation(mutating:) takes a reference to the struct and the changes of the original are reflected in the default implementation calls even after enabling default implementation.
We recommend using the non-mutating method for enabling default implementation unless you need to track the changes for consistency within your code.
Chain stubbing
It is possible to chain stubbing. This is useful for when you need to define different behavior for multiple calls in order. The last behavior will be used for all calls after that. The syntax goes like this:
when(stub.readWriteProperty.get).thenReturn(10).thenReturn(20)
which is equivalent to:
when(stub.readWriteProperty.get).thenReturn(10, 20)
The first call to readWriteProperty will return 10 and all calls after
Related Skills
node-connect
334.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
82.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
334.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
82.3kCommit, push, and open a PR
