📈 Unit Testing in Swift: Proper Architecture
Welcome to the third part of the Unit Testing in Swift series, where we will cover how to improve your overall unit test quality with a good architecture throughout your project. If you are new to unit testing and want to learn how to start unit testing with Swift in Xcode, I suggest you take a step back to Part 1: The Fundamentals of the series, before reading further. This part of the series is for you, if you want to learn how to implement certain design patterns that allow you to be in complete control of your unit tests, without having to modify or compromise existing code. The content within this part will be on a more abstract level, hence you don’t necessarily need to be a Swift developer to find it useful — good architecture goes for any software development project.
Be In Control!
Let’s start this article by looking at what being in control means, when it comes to unit testing. Looking at the figure below, we see three classes:
A has a dependency to class
B, which in turn has a dependency to class
C. The only class not having any dependencies is class
Now, since class
C has no dependencies we are able to easily test it without having to worry about data coming from an external source. Unfortunately we can’t say the same for class
B. When testing class
A, we would receive data from class
B that may be inaccessible to us and hence have no control of the actual scenario that we are trying to test. In fact, testing functions on class
A depending on functions within class
B depending on functions within class
C is normally referred to as integration testing of the module containing all three classes. Unit testing focuses on a lower level, namely each unit.
In unit testing, we must isolate the tested class in order to be in control. In our example this means that we must cut any ties from class
A to the real class
B in order to test class
A. Instead class
A should depend on a mocked version of class
B that we are in control of. Likewise, in order to test class
B we must cut the ties to the real class
C. If this sounds confusing to you, don’t worry — we will cover how to do this in a minute, but it is necessary to understand that, when unit testing, the most important rule is to control the scenario by eliminating any external dependencies from the code that we are testing. If you already know how to enable isolation of the testable classes and want to learn how to properly mock a dependency, I recommend jumping straight to Part 4: Mocking.
Isolating Testable Classes
Enabling the isolation of each testable class within your project is not as difficult as it sounds. We simply have to follow a couple of well known design patterns. Let’s get a bit more concrete with a typical example of how classes
C can be implemented in a less unit friendly way:
From the concrete code example above, we see that class
A is initialising an instance of
B is using the shared singleton instance of
C. This leaves us with no control of the dependencies for each class, at all. Let’s say we wanted to write unit tests for the
validateAnswer() function of class
B. Since the shared instance of
C always returns
42 we will not be able to test any other scenario, leaving us with a very low quality test. It’s time to fix that with our first design pattern.
The first and foremost pattern to introduce to your project, is initialisation-based dependency injection. This pattern makes sure that all dependencies of a class are provided to the class upon initialisation or when calling exposed functionality, freeing the class from any responsibility of creating the dependencies itself. This in turn allows us to decide exactly which instances of the depending classes to use — or in other words; being in control of the scenario. Let’s have a look at how we can improve the classes
B with dependency injection:
By injecting an instance of
B respectively, we can now access and modify the dependencies from our unit tests. These are both examples of injection on initialisation level, however injection can also occur on function level. Remember our issue with
DispatchQueue from Part 2: Asynchronous Expectations? The solution to the issue was to inject the queue directly into the function enabling us to synchronise the queue from within our tests. For a concrete example of this, please see Part 2: Asynchronous Expectations.
Dependency injection alone, however, does not completely solve our problem of not being able to test any other scenario than
42 for the
validateAnswer() test, since we are still only able to use a concrete instance of class
C for the tests. To deal with this, we need to introduce another design pattern.
It’s just a Façade
The facade design pattern will help us solve that problem by abstracting the functionality of the dependencies. As the name implies, we create a facade of the functionality. Translated to Swift, this means that we create a protocol exposing the required functionality of the dependencies that can then be implemented by our real depending classes and custom mock classes that we will create solely for the purpose of testing. In addition to improving testability of our code, the facade pattern also improves the general understandability of the project by hiding the complexity. Due to the power of this pattern it is also mentioned in the legendary Design Patterns book by the Gang of Four.
Let’s see how the facade design pattern can be implemented in our example:
By creating a protocol for each of the injected classes, we abstract the functionality. This protocol can then be implemented by the original classes. Note that for the purpose of the example I have renamed the classes to
ConcreteC. Some prefer suffixing the protocol names with
Type instead, allowing the concrete implementations to keep the original name.
Notice how the classes
B refer to the abstracted facades of
C respectively. With the protocols for
C defined, we can create our own mock classes and start testing any scenario that we can imagine. And this was all done without compromising any of the classes — the properties are still private and only the required functionality is exposed.
While each of the two above-mentioned patterns are great to introduce to your project, you will find that one does not really make sense to introduce without the other. Refactoring a whole project that has never been using the patterns before can be a cumbersome task, but trust me when I say that it is well worth the time and effort in the long run.
A note on Singletons
If you’ve reached this part of the article you will probably already have discovered that the singleton design pattern looses some of its power with the introduction of the dependency injection and facade patterns. In my opinion though, it ensures that the singleton pattern is used exactly the way it was intended to. In way too many projects I see an abuse of the singleton pattern with singleton instances being referenced to directly from within functions, making them impossible to test properly. Having shared (single) instances of some classes is a real requirement that we must support, but instead of referencing them from everywhere, start injecting them throughout the flow of your application. I dare to say that in time you will feel much more in control of the code and as a result of this, see an overall increase in the code quality and coverage of your project.
Next step: Mocking!
The only thing left to do now, is to mock the dependencies for each of the testable classes, in order to test them properly. While mocking in itself is a very simple technique, there are things we can do to additionally improve our unit testing with mocking. All of this will be covered in the next part of the “Unit Testing in Swift” series.
As always, if you have any questions or comments, feel free to reach out to me by commenting on these articles. I will reply to all messages.