Hunting down phantom mock instances in XCTestCases and Cuckoo
Sharing another simple lesson and insight learnt when chasing a mysterious issue
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 XCTestCase
s. (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 nil
ed 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 nil
ing 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!!!