Unit Testing in Swift with Dependency Injection
Dependency injection is one of those things in iOS development that are avoided by many developers when we have to do with native APIs. One typical example is UserDefaults.
We rarely, if ever, have to replace them with another third party API so many choose to call them directly because it is virtually improbable that they will ever switch to another API.
But dependency is not only handy when we want to future-proof our API-consuming component. It is also of vital importance when we unit test. (You do this, right? :-P )
To be clear, for anyone new to the concept of dependency injection, what is meant by it in the context of UserDefaults, is that your APIConsumer class does not call directly e.g. UserDefaults.bool(forKey: “property”) but instead have this: storage.bool(forKey: “property”) where storage can be either UserDefaults or any other API that exposes the same methods.
In third-party APIs this strategy enables you to easily swap an API with another without needing to look for all places where you call API X and change it to API Y.
However, with iOS native APIs I mostly implement dependency injection in order to better unit test my code. If instead of UserDefaults I have my code to use data from storage then when unit-testing I can easily use a mock storage object which any functionality I want it to have.
The way to do this in Swift is to make your storage property not draw its type from a concrete class but from a protocol. This way, any class that conforms to this protocol can act as the real UserDefaults.
The neat thing with Swift is that you can make UserDefaults conform retroactively to your storage protocol by extending it!
So by looking at the code below we can break down the dependency injection into the following points:
We see our Main class which depends on a service which will be used in a static way. This creates some inconvenience especially in testing later but all we need to do here is just use .self to access the static part instead of the instance.
In order to make Main depend on a protocol first we declare ServiceProtocol which (conveniently!) happens to have the same method that RealService exposes. And then see that (even more conveniently!) RealService is extended to comply to this ServiceProtocol.
So what remains is to define the type of the service property to be ServiceProtocol.Type and even provide a default value for this (the real service) so that unaware users end up with using real stuff by default.
Now moving to testing Main we see that things have become quite easy:
We have initiated our class in test in the component property and we have replaced its service with our MockService.
The only thing we have to take care is that because our dependency is static we have no way to store a variable inside the MockService to be examined later. We can solve this easily by creating the TestData singleton exclusively for storing test result data and then comparing the values there!
So, now that we saw how Swift enables easy dependency injection without extra frameworks (looking at you Kotlin…) we can begin practicing it a little more often!