Hunting down phantom mock instances in XCTestCases and Cuckoo

Sharing another simple lesson and insight learnt when chasing a mysterious issue

Image from Unsplash

In the last post, I shared a demonstration and a lesson that you always have to nil out instance variables in tearDown method in your XCTestCases. (You don’t need to go back to my last post if you landed here first. The bold statement above is the only thing you need to remember, really)

In this post, I will share another interesting case/lesson, which is specific when using Cuckoo.

Conclusion

Let’s start from conclusion this time. Sometimes, tonil out instance variable is not enough to evade from leaking mock object(s). You need to call Cuckoo.reset(...) to reset the mock object.

Sample Project

I included the demonstration code in the same sample project as last time.

In this example, we have two actor classes: Parent class and Child class. Parent references a Child, Child references a Parent but these references are abstracted at the protocol level.

As you can see immediately from the class diagram, this bi-directional dependency would cause cyclic reference. To avoid it, at the class level, the Child references ParentProtocol asweak. But unfortunately, we cannot declare weak references in protocols in Swift, and that’s what would cause the problem.

Cuckoo generates MockXXX classes only from protocol information, so this weak reference you implemented at the class level would not carry on to the MockXXX classes. As a result, MockXXX classes will have cyclic references and leak.

Demonstration

class ParentTests: XCTestCase {  var child: MockChildProtocol!
var sut: Parent!
override func setUpWithError() throws {
child = MockChildProtocol()
Cuckoo.stub(child) { stub in
when(stub.name.get).thenReturn("Michael")
when(stub.parent.set(any())).thenDoNothing()
}
sut = Parent(name: "Kurt", child: child)
}
override func tearDownWithError() throws {
child = nil
sut = nil
}
func testIntroduce() {
// Given:
// When:
let text = sut.introduce()
// Then:
XCTAssertEqual(text, "My name is Kurt. I have a child named Michael")
}
func testRescueChild() {
// Given:
Cuckoo.stub(child) { stub in
when(stub.rescued()).thenDoNothing()
}
// When:
NotificationCenter.default
.post(name: childCryingNotification,
object: self)
// Then:
verify(child).rescued()
}
}

You don’t need to care about the detail of the test code. In the above XCTestCase class, we have two test methods and we have a Parent object and aMockChildProtocol object as instance variables. Unlike in the last post, I have already niled out all instance variables of the test class, so leak should not happen from those. (yeah, SHOULD not…)

Each test will succeed if executed individually. However, it will fail if you run the entire test file in sequence:

The error says No stub for method 'rescued()' but you can see that I have stubbed that method right above (L45). Familiar mystery!! 🕵🏻‍♀️

The error is actually happening because the leaked child from previous test case is missing stub method:rescued(). Why does thechild leak? Because of the cyclic reference created by Cuckoo as I explained above.

Solution

The solution is very simple. As I already stated in the above, you just need to call Cuckoo.reset(...)

override func tearDownWithError() throws {
Cuckoo.reset(child)
child = nil
sut = nil
}

just call Cuckoo.reset(child) before niling out instance variables in tearDown, and your entire test suite runs happy now 💥🎉🎊

Final thought

Even though resetting mocks are briefly covered by Cuckoo’s official README, it doesn’t say WHEN actually we need to call this. I guess the best practice is to call it on every mock instance when not needed, but it can become a bit hectic and tedious. I hope this post will give you a better insight as of when you should call Cuckoo.reset. Happy coding!!!

Coding in Swift, want to learn Flutter, a Dad, a Rock Climber, learning Parkour and Guitar

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store