Stubbing in pair with Swift compiler: a spy registration

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

Bartosz Polaczyk
Apr 8, 2019 · 6 min read
Image for post
Image for post
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

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

Step 1: stub split

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

Nice mock stubbing

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

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

Image for post
Image for post
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

Improvement 1 — support for throwing functions

Improvement 2 — generics functions

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

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

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.

Image for post
Image for post

Flawless iOS

🍏 Community around iOS development, mobile design, and…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

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