Mocking with Cuckoo
Swift 5’s lack of reflection makes generating mocks at runtime all but impossible. If you have gone all in on protocols, then your life is slightly easier, however you still need to manually create mock objects for each protocol you want to use.
At Volley we use Cuckoo to generate mocks for our classes, structs & protocols. It installs itself as a build step before complication, and generates the source for the mocks which can then be compiled with your tests enforcing compile time safety.
While it’s pretty cool it has a couple of annoying limitations.
Inheritance
Cuckoo uses SourceKitten to parse and interpret the input files you select. As it is only parsing the file, it’s not aware of methods that are declared on the superclass but not overridden in the subclass. To better understand what this means in practice let’s use the example below.
/// Parent.swift
class Parent {
func a() -> String {
return ""
}
}/// Child.swift
class Child: Parent {
func b() {
}
}
If we provide Child.swift
as an input file, Cuckoo will create a mock called MockChild
that will only implement b()
as SourceKitten can’t see a()
because it’s not in the same file. Now, because the MockChild
class is a subclass of Child
calling a()
will still work, however we can’t stub it’s value, and all calls to that method on the mock will call the actual implementation.
If we want to stub a()
we do that by overriding it in Child
. As SourceKitten is only reading the source, we only need to implement the signature.
/// Child.swift
class Child: Parent {
override func a() -> String {
return super.a()
}
func b() {
}
}
Great, so now Cuckoo will generate Child
mocks where we can stub both methods. But we now need to do this in our source, everytime we want to stub a method from a ancestor class, which is far from ideal.
3rd Party classes
There are plenty of times where you’ll want to test how your code behaves in response to changes in 3rd party code.
In the Volley app we use a WKWebView
and in certain situations we want to communicate between the web app and the native app. WKWebView
allows you to assign a delegate that is called with a WKScriptMessage
every time a JS event is fired on the webpage.
Cuckoo only accepts filenames as input, not classes or symbol names. There is no obivious way for us pass the WKScriptMessage
class to cuckoo, or is there…
Well, we can just use the technique that we introduced above to work around this. All we need to do is to define a class that subclasses the 3rd party target class, and override the methods and/or properties that we want to stub. We can then provide this file as an input to Cuckoo.
We chose to suffix all of these proxy classes with proxy
so they are easier to distingish from the framework’s class. However thanks to namespacing this is optional.
/// Proxies.swift
class WKScriptMessageProxy: WKScriptMessage {
override var name: String {
get {
return ""
}
}
override var body: Any {
get {
return ""
}
}
}
The actual values returned in the definition are not important as the Cuckoo generated class (MockWKScriptMessageProxy
) will allow us return any value of the same type.
We can now create a test that looks like:
/// WebViewTest.swift
class WebViewTest: XCTestCase {
func testWebViewEvent() {
// A class that implements WKScriptMessageHandler
let subject = WebViewDelegate()
let contentController = WKUserContentController()
// Our 3rd party mock
let message = MockWKScriptMessageProxy()
// configure a our stubbed values
stub(message) { stub in
when(stub).name.get.thenReturn("eventName")
when(stub).body.get.thenReturn(["key":"value"])
}subject.userContentController(contentController, didReceive: message)
/// verify how the message was handled
}
Conclusion
The above approaches help solve some of the issues when testing with Cuckoo. Sadly they are far from perfect, as they have the side effect of forcing you to modify your implementation to support testing and/or write a lot of boilerplate to test 3rd party code.
One improvement that we’ve considered using a runtime headers dump to generate the proxies. There are a number of issues with this, however it would at least eliminate the need to manually create the mocks for UIKit.
In reality the limitations are not with Cuckoo itself, but with SourceKitten, and ultimately Swift’s lack of reflection API. Lets hope the Swift team come up for the next release.