Mocking with Cuckoo

Using the Cuckoo mocking framework as Cocoapods dependency

Serge Mata M
Mock with Cuckoo
6 min readAug 29, 2019

--

Setting up

Compatibility

Source code and official notes for Cuckoo can be found https://github.com/Brightify/Cuckoo.

At the time of the redaction of this article, there is support for Swift 4 and Swift 5. You will have to use the version compatible to the Swift version that you are coding in.

Limitations

  • inheritance (grandparent methods)
  • generics
  • type inference for instance variables (you need to write it explicitly, otherwise it will be replaced with __UnknownType)
  • struct — workaround is to use a common protocol
  • everything with final or private modifier
  • global constants and functions
  • static properties and methods
  • protocol inheriting NSObject
  • does not expect throws, you need to surround the throwable code with a `try block`

Adding Cuckoo as a test target dependency

Add as one of the dependencies to your test targets, in your `Podfile`.

target ‘YourTestTargetAppTest’ doinherit! :search_pathspod ‘Cuckoopod ‘YourAnotherTestTargetDependencyend

Adding an initial mock file(s) to your test project

Cuckoo will generate one or mock files. However, those will not be automatically added to your test project.

It is convenient that you start by creating the initial files yourself as part of your test projects.

A good practice is to have the same number of mock files as the number of modules that you are mocking the classes of.

Let’s say, we want to mock User.swift that resides in the Core module, and we want to mock CardHolder.swift and BankDetails.swift that reside in Card module, I highly recommend that you have two files to hold each the mocks of classes belonging to the same module.

  • CardMocks.swift that will contain the mocks for CardHolder.swift and BankDetails.swift
  • CoreMocks.swift that will contain the mock for User.swift

And so on, you can have as many n files as mock container files as needed.

To start, add the n number of blank swift files to test target YourTestTargetAppTest. In our case, n = 2 with CardMocks.swift and CoreMocks.swift. It might be necessary to create all of them inside a directory which can be called CuckooMocks or AutogeMock.

It is important to compile the test target at least once, to ensure that everything is linked to the correct target.

Generate mocks

Custom mock generation script

Mock classes should be generated before the compilation of the test classes. You can find a way of generating Mock on the Cuckoo GitHub page. Here is a script that can be used to find listed file and generate respective mocks. therefore the following script has to be the first build phase of your test app target. This will ensure that the mock classes exist prior to the compilation of any tests where they are needed.

#Author: Serge Mbambatrap "exit 1" TERMexport TOP_PID=$$CARD_MODULE_TEST_TARGET_NAME="CardApp" # The name of your test AppCARD_MODULE="Card" # One of the dependencies, or one of the imports needed for your mockCORE_MODULE="Core" # One of the dependencies, or one of the imports needed for your mockOUTPUT_FILE_CARD_MOCK="$PROJECT_DIR/CardAppTests/CuckooMocks/CardMock.swift"OUTPUT_FILE_CORE_MOCK="$PROJECT_DIR/CardAppTests/CuckooMocks/Core.swift"echo "Cuckoo genereted mocks are in $OUTPUT_FILE_CARD_MOCK, $OUTPUT_FILE_CORE_MOCK" # Telling you where your mocks areNON_DEV_PODS_DIR="$PODS_ROOT"DEV_PODS_DIR="$SRCROOT/../path-to-source-files/Card"echo "Mocks Input Directory = $NON_DEV_PODS_DIR and $DEV_PODS_DIR"function findFileInNonDevPods() {cd "$NON_DEV_PODS_DIR"SEARCH_DIR=$(pwd)filename="$(find $NON_DEV_PODS_DIR -name $1)"numberOfFiles="$(echo $filename | grep -EIho '.swift' | wc -l)" # Count the number of matches for the provided fileif [ $numberOfFiles = "1" ]; thenecho "$filename"elseecho "Failed to found unique file $1 in $SEARCH_DIR, found: $numberOfFiles" # Breaks because the file with the provided name was not found or found more than oncekill -s TERM $TOP_PIDfi}function findFileInDevPods() {cd "$DEV_PODS_DIR"SEARCH_DIR=$(pwd)filename="$(find $DEV_PODS_DIR -name $1)"numberOfFiles="$(echo $filename | grep -EIho '.swift' | wc -l)" # Count the number of matches for the provided fileif [ $numberOfFiles = "1" ]; thenecho "$filename"elseecho "Failed to found unique file $1 in $SEARCH_DIR, found: $numberOfFiles" # Breaks because the file with the provided name was not found or found more than oncekill -s TERM $TOP_PIDfi}${PODS_ROOT}/Cuckoo/run generate - testable "$CORE_MODULE", \- output "${OUTPUT_FILE_CORE_MOCK}" \"$(findFileInNonDevPods User.swift)" \"$(findFileInNonDevPods FeatureFlagProvider.swift)"${PODS_ROOT}/Cuckoo/run generate - testable "$CARD_MODULE" \- output "${OUTPUT_FILE_CARD_MOCK}" \"$(findFileInDevPods CardFactory.swift)" \"$(findFileInDevPods CardHolder.swift)"

What you need to understand about the script :

- You will have to update the value of `DEV_PODS_DIR` to point to the correct directory where the source files of your dev Pod is found.

- The line $(findFileInDevPods User.swift) will generate mocks for all classes and protocols found in the file User.swift. Add a similar line with your filename to mock other classes or protocols.

- If you want to mock classA that inherits from classB, you will have to include both the filenames where classA and classB (in the script) are found.

- When adding a new file for mocking, remember to have a space before the backslash and enter the file on the new line. The last file entry does not need a backslash.

Main features

There are certainly a lot more useful features in Cuckoo. I am only highlighting a few of those.

1. Spying on super (only for class mocks)

Assume the class

class Person {var name: Stringvar surname: Stringvar middleName: String?let dateOfBirth: Datevar siblings: [Person]var spokenLanguages: [String] = []init(name: String, surname: String, dateOfBirth: Date, middleName: String? = nil, siblings: [Person] = []){self.name = nameself.surname = surnameself.dateOfBirth = dateOfBirthself.middleName = middleNameself.siblings = siblings}func addSpokenLanguage(languageName: String) {spokenLanguages.append(languageName)}public func say(word: String, language: String) throws {guard spokenLanguages.contains(language) else {throw SpeakingError.doesNotKnowLanguage}try produceSound(for: word, inLanguage: language)}func produceSound(for word: String, inLanguage languageName: String) throws {// Some complicated things with bytes and AVAudiodevice her}}

If you generate a mock for `Person` with Cuckoo, it would be named MockPerson. The following line allows us to create a mock instance for the MockPerson class that has the very same behavior of an instance of Person for any properties/getter/setter/methods that are not stubbed explicitly.

var mockedPerson = MockPerson(name: “Bob”, surname: “Marting”, dateOfBirth: Date.init(timeIntervalSince1970: 0.0))mockedPerson.addSpokenLanguage(languageName: “English”)

2. Stubbing

When we stub, provide a different behavior for a property/setter/getter/method, etc.

  • Stubbing a getter:
let expectedSurname = “Martin”stub(mockedPerson) { mock inwhen(mock).surname.get.thenReturn(expectedSurname)}

This will ensure that the value of expectedSurname is always returned when getting the surname property from the mockedPerson.

* Stubbing a method:

In a test, you do not want to execute the actual produceSound implementation as this interacts with hardware and so on. You only need to know whether the method was called.

stub(mockedPerson) { mock inwhen(mock).produceSound(for: any(), inLanguage: any()).thenDoNothing() // Stubbing the actual implementation with a no-operation implementation}do {try mockedPerson.say(word: “Fine”, language: “English”) // Making a triggering call} catch { }verify(mockedPerson, times(1)).produceSound(for: “Fine”, inLanguage: “English”) // Asserting that the method was called, without actually performing it.

3. Expect invocation

The line

verify(mockedPerson, times(1)).produceSound(for: “Fine”, inLanguage: “English”)

above is expecting an invocation of the produceSound method with the given parameters only once. In the above example, the matching of parameters is guaranteed because they are primitives adhering to Equatable. If you are matching your own class, you need to use Cuckoo’s `ArgumentCaptor` or make the class conform to Cuckoo.Matchable.

When a method call is expected to happen once, then the second parameter of verifying can be omitted. In the above example, the verification line can be rewritten as:

verify(mockedPerson).produceSound(for: “Fine”, inLanguage: “English”)

4. Reject invocation

We could test that a certain method is never called:

do {try mockedPerson.say(word: “Fine”, language: “Chinese”) // Non triggering call} catch { }verify(mockedPerson, never()).produceSound(for: any(), inLanguage: any()) // Assert that productSound is never called

When testing for no invocation or rejection, I recommend using any() since that would ensure the method was indeed never called, irrespective of the values of the parameters. The value represented by any() can be any possible value of that parameter.

You can specify actual parameter values, other than any() in the reject call whenever you expect the call not to happen only for those values.

5. Reset invocations count

When we know that a certain method will trigger an invocation and we want to assert that another method will trigger no invocation, we will need to clear all invocations prior to adding the code that is supposed to be `non-triggering:

do {try mockedPerson.say(word: “Fine”, language: “English”) // Triggering call} catch { }verify(mockedPerson, times(1)).produceSound(for: “Fine”, inLanguage: “English”)clearInvocations(mockedPerson) // Ensure that invocation count is set to 0do {try mockedPerson.say(word: “Fine”, language: “English”) // Non triggering call} catch { }verify(mockedPerson, never()).produceSound(for: any(), inLanguage: any())

The line clearInvocations(mockedPerson) clears all invocations.

Read the official docs

I have tried to summarize on Cuckoo’s main features in this article. Please read the official at https://github.com/Brightify/Cuckoo for a thorough understanding.

--

--