Stubbing in pair with Swift compiler: a spy registration

How to effectively verify unit test side effect without code generation in Swift

Thanks to Bill Chung for the wonderful eye animation

Often we need to unit test code with side-effects that interact with other parts of your system using abstraction — protocols in Swift. There are many techniques to build that abstraction and verify if our code works as expected: stubbing, spying, dummying etc.

Previously, I made a deep dive into auto-generating a stub placeholder that allows seamless customization of a mock’s function behavior right in a test case. Let’s extend that approach with a spy registration that observes and records side-effects generated during a test case.

What you need to know first

Let me present in a nutshell how you can speed up a process of building function’s stub with some Swift compiler help (for details refer to my previous post).

Assume that you wish to create a mock for a protocol containing some function (for the sake of this article named addUser(name:)). And instead of creating it from scratch with a custom flag(s) and/or embedded assertions, for each function generate only a single variable as a placeholder to be called internally during that function’s call. In a traditional way, you would need to manually provide a type of that placeholder's function but with lazy modifier and some simple helper function stub, it can be inferred by a Swift type system. The final solution would look as simple as:

In a test case, you need to explicitly register behavior of addUser with a concrete implementation, like:

Problem statement

The benefit is that we don’t have to manually specify the type of addUserAction variable. However, generating its body (addUserAction) has to be done on a test case-level which often leads to messy variable declaration and assignments as above. The aim of this article is to completely eliminate that manual step of action declaration which in a first place can positively affect test case readability and as a by-product positively affect development speed and joy.

Step 1: stub split

Firstly, let’s modify our stub function and separate its input and output types into two generic types:

it works also for functions that take several parameters —then the type will be a tuple with corresponding types, like:

Nice mock stubbing

Once we split the type of our entire stub’s type into Input (I)and Output (O) generics, as a low hanging fruit we can make a stub builder that generates nice stub — one that returns some predefined value:

Once action placeholder is build using niceStub, in a testcase we don’t have to manually “register” the function’s body before calling the protocol’s function:

Another simple improvement here is defining predefined return values for some popular types (like 0 for Int, false for Bool, nil for Optional<T> etc.). With a simple DefaultProvidable protocol that specifies a default value of a given type and more specific niceStub, stubbing is as simple as never before:

What if the function doesn’t return anything? In a Swift language all functions actually return something, Void if not specified. Void is implemented as a zero-length tuple and as a non fully fledged type it cannot conform to any type like DefaultProvidable. No worries, Swift type system can correctly select the right implementation if we introduce another niceStub specifically for functions that return Void:
func niceStub<I>(of: (I) -> Void ) -> (I) -> Void {
 return { _ in }
}

Call arguments spy

Although we have a simple way to define stub, we completely ignore passed arguments there. However, it not uncommon to inspect arguments that have been passed to the mock object and to achieve that we need to apply another trick. Thanks that body of a mock’s function (like addUserAction) is defined as a variable we can easily edit its behavior in a testcase runtime.

To apply a spy, just inject an intermediate spy layer that stores all the arguments call in a recorder and performs already specified body.

Injecting spy layer using spyCalls(…)
If you are not familiar with Swiftinout modifier, it gives a change to modify the value of passed argument once it is finished. It resembles a bit passing pointer argument in Objective-C but with a restriction that it cannot escape that “pointer”.

spyCalls takes an inout argument of a function to spy, injects a function that records its arguments to ArgRecords<I> and then calls original stub implementation.

Implementation of a missing ArgRecords is really straight-forward as this is just a custom collection to keep track of all records. It needs to be represented as a reference type collection and backing up by the simplest Array is often enough (as long as you don’t expect any multithread read/write operations).

ArgRecords resembles a Collection but here it specifies custom subscript return type. Instead of T, I suggest returning optional T? just to not crash tests if the expected spy index is out of range. In practice, subscript result lands in XCTAssertEqual which automatically aligns optionality types and gracefully could handle out-of-range access:

Technically, subscript could return non-optional T and conform to Collection but any out-of-range access crashes the test suit —it is up to you what type to select.

Further improvements

If you find it useful and promising, this is just a beginning! The presented approach sets up a foundation for much more complexed and omnipotent testing toolset that supports synchronous or asynchronous expectations, return values control with some syntactic sugar API. Open-source StubKit library is based on that approach and provides a set of extensions (some of them are described below).

Improvement 1 — support for throwing functions

Until now we assumed that our stubbing function does not throw. If it does, we need to prepare twin declarations of stub and spyCalls that are capable of stubbing and spying functions with throws modifier:

Improvement 2 — generics functions

Creating a stub/mock of a function that is generic is a bit tricky. As long as you expect only one specification, your entire mock can be generic with a force-casting (aka unwrapping):

However, if a single generic type for a stub is unacceptable, you should prepare a set of expecting specializations and choose one that matches. Please refer to StubKit’s documentation for detailed example.

Improvement 3— Convenient ArgRecords comparison

So far in the assert section we had to manually compare each subsequent call manually:

With straightforward conformance to Swift’s ExpressibleByArrayLiteral protocol, ArgRecords can be initialized directly from an array literal (like [1,2])

Furthermore, if we spy a function that takes a single Equatable argument, entireArgRecords can easily conform to Equatable:

By wrapping these two improvements, the Swift compiler automatically aligns appropriate types and makes such idiomatic assertion possible:

Summary

Writing and working with a manually created stub or mock seems to be a bit boring and wearisome step. Contrary to other languages (Ruby or JavaScript) lack of type dynamism API in Swift prevents more flexible and automatic testing tool. Technically Swift ABI lays some groundwork for magical testing library, but as Doug Gregor wrote, “it would be a bunch of work (…) yet doable”.

I believe that the presented in this article approach eliminates some degree of dummy code that has to be written, although some minimal work to prepare a mock/stub is still required. The greatest benefit is concise and self-explanatory test case without a flood of intermediate variables placed around stubs and test case scope.