unit testing: how

17 February 2018

Introduction

When you are reading articles on TDD or unit testing in general sometimes it may feel like they have little practical value, mostly because they tend to focus on trivial examples, having tests like "2 + 2 = 4" or "when you push something onto stack it is not empty". It's easy to write tests like this, and all is well. However, in real projects things are not that easy. So, I wanted to dedicate this note to which properties of a system-under-test (SUT) help with testing.

What is Testability?

I would like to start with a definition of testability that will guide our explorations later:

The level of code testability shows how easy it is to recreate a deterministic environment for test to run in and produce its expected outcomes.

So, as I mentioned in previous notes, I find it extremely important to have unit tests that are deterministic in order to maximise their value. In other words, everything that hinders your ability to recreate the needed environment for executing tests is decreasing the testability.

Over the years I have come up with the following two main obstacles getting in the way of testable code.

Implicit Dependencies

Implicit dependencies (or, rather, implicit mutable and / or "side-effectful" dependencies) are the most common thing that impedes your ability to write useful tests. Going back to the example in the beginning of the note, why exactly is add(2, 2) == 4 is easy to test? It's because the result of the addition function (hopefully!) does not depend on anything except its arguments (in other words the function is pure), so you can recreate any test scenario just by supplying corresponding arguments to the function.

With classes or structs things get only marginally more complicated, if we remember the fact that methods are nothing more than free functions that have an implicit first argument, corresponding to the instance (and, transitively, to all its properties). So, when you have control over all properties of a SUT either directly (via constructor and / or accessor methods) or indirectly (via other public methods), writing unit tests is just as easy as testing that add(2, 2) == 4.

Most common implicit dependencies include (sorry, this list is highly biased towards Apple platforms, but I'm sure developers from other platforms can easily come up with relevant examples):

and so on.

Another category of implicit dependencies, that is somewhat more sneaky, is non-pure free functions, like libdispatch, threading or AddressBook APIs. I'd argue that this kind of dependency is even worse, because, while with object oriented APIs you can at least mock and stub things, there is no way you can affect behaviour of these functions from your tests (apart from being too clever with patching your binary images).


It is certainly possible to test code that contains implicit dependencies, the experience of writing and supporting such tests leaves a lot to be desired. You have to be really careful to mock and stub all implicit dependencies of an SUT at the beginning of the test, and then not forget to undo all your modifications to these implicit dependencies, so they don't affect tests that will be running next. This can have even worse consequences if you allow other code to run while running your test (for example, by manually spinning a run loop, which will call out to its sources), and this code in turn uses the same implicit dependencies, it may be caught completely off guard by the mocking or stubbing. Finally, since there's no indication of what exactly needs to be stubbed or undone (these are implicit dependencies after all!), you need to figure this out on your own.

The first time I tried to eliminate all implicit dependencies from my code, it immediately became easier to test any aspect of behaviour that I wanted. But at the same time I noticed something else — the number of arguments for my constructors also increased dramatically because of all this explicit dependency passing (which is quite obvious). We will get back to this as we discuss the second main property of SUT which affects testability.

Intermixing Object Construction & Logic Under Test

It is almost universally true that in any given unit test you need to at least instantiate your SUT. The harder it is, the more difficult it is to write tests, so it makes sense to look into this in more detail.

Let's say you have a Document class in your application and it has the following API:

class Document {
    let html: String

    init(url: URL) {
        let client = HtmlClient()
        html = clientget(url)
    }

    ...
}

So, you give it a URL, which is used to retrieve the corresponding HTML string, that gets saved in a private property of the Document class.

The problem with this code is that it is not that easy to test. In order to do this, you may need to set up some network layer stubbing or even start a local web server, so you can pass a localhost URL to the constructor of the Document. All this needs to be done even before you start writing the actual test code.

Following our rule of no implicit dependencies, we can try to solve this by passing an instance of HtmlClient as a parameter of the constructor:

init(client: HtmlClient, url: URL) {
    html = client.get(url)
}

This is making things a bit easier, since you can just pass in a mock HTML client, that returns a needed string from its get method. But this is still more cumbersome than it needs to, since we can just pass an HTML string itself:

init(html: String) {
    self.html = html
}

Now, you can just create a string in a test and pass it directly to the constructor. No need for mocking, stubbing or running web servers.

I'd argue that the real problem with this piece of code was that it didn't actually need an URL or a HTML client. All it is interested in is the HTML string, and it doesn't matter where it comes from. Also, note how creating even a single object inside SUT makes it a lot harder to test, since there is no way you can control this instantiation from a test and recreate the needed test environment.

Let's have a look at another example, similar to what I mentioned at the end of previous section. Suppose we have a fairly ordinary UIViewController subclass in our application, and, since we followed the advice on not having implicit dependencies, everything is passed from the outside:

class MyVC: UIViewController {
    var dataSource: UITableViewDataSource!
    var delegate: UITableViewDelegate!
    var tableView: UITableView!

    let provider: DataProvider
    let router: Router

    init(provider: DataProvider, router: Router) {
        self.provider = provider
        self.router = router
    }

    override func viewDidLoad() {
        dataSource = DataSource(provider: provider)
        delegate = Delegate(router: router)
        tableView = UITableView(frame: .zero)
        tableView.dataSource = dataSource
        tableView.delegate = delegate
        ...
    }
}

Here we're passing some sort of data provider which will be used to populate the table view, and a UI router that is responsible for displaying some other screen when a certain row in the table view is tapped. These two objects are retained in order to use them later for constructing table view delegate and data source, and then assigning them to the corresponding table view properties. It might not make a lot of sense on its own, but it mostly serves to illustrate a bigger idea: the two dependencies, that are passed to the constructor, are only used to instantiate internal objects, which are in turn passed to the table view. Note that none of these objects are being directly used by MyVC and are not strictly necessary for its functioning. You would get the same result by having this code instead:

class MyVC: UIViewController {
    let tableView: UITableView

    init(tableView: UITableView) {
        self.tableView = tableView
    }
    ...
}

(Not to mention that you no longer need optionality of tableView property). If it is really important to instantiate table view in viewDidLoad, you could pass the factory instead:

class MyVC: UIViewController {
    var tableView: UITableView!
    let tableViewFactory: () -> UITableView

    init(tableViewFactory: () -> UITableView) {
        self.tableViewFactory = tableViewFactory
    }

    override func viewDidLoad() {
        tableView = tableViewFactory()
    }
    ...
}

The main idea is that MyVC is no longer burdened with responsibility of instantiating dependencies of its children, but is dealing only with dependencies that are strictly necessary for its work. You may go even further and notice, which exact interface is required from tableView by MyVC, and pass this interface instead of concrete type, which will make this class even easier to instantiate and test.

At this point you may be wondering, but who is actually creating data sources, delegates, table views and so on? This is a question that I'm going to talk about in the next note, so stay tuned for part III!