I joined a project that follows a modular approach in a VIPER architecture. Our project is divided into smaller subprojects, with each code segment relying on a set of services to function properly. Consequently, we needed to implement a way to create mocks for the different services we utilize. For example, if we have a presenter in module A that depends on the router A of the same module, and we wish to test a code snippet within the presenter that calls a method from this router, we will need to create a mock version of the router for testing purposes. Additionally, we had to generate fake data to easily obtain specific data for each test, even when dealing with scenarios involving nested objects and numerous attributes.
There are libraries available for mocking services, such as Cuckoo, but they have certain limitations that prevent their use in this project. One notable limitation in our case is the inability to directly mock classes marked as final. Indeed, in the architecture of our project, since each module needs to be independent, we do not want any element to be consistently inheritable to avoid the temptation of over-sharing code parts between each module. Additionally, defining classes as ++code>final++/code> when applicable also simplifies the compiler's task and thus saves some compilation time, especially on big project 😉.
This article will explain which strategy we implemented to overcome this deficiency and show how to generate data for two purposes:
Throughout this article, we will use a straightforward application as an example, mimicking a ++code>TODO++/code> application’s functionality in a VIPER architecture. The app uses an API call to retrieve a list of tasks which can be accessed through a service injection, a component to display the list, and a ++code>Task++/code> class to represent each task.
Our first objective is to add some tests in order to test specific part of the app. To achieve this, we need to simulate the behavior of our application as closely as possible to its actual behavior, while abstracting external dependencies. There are various ways to achieve this, such as overriding methods or implementing a protocol, but in this example, we will focus on how to do it using a protocol.
This protocol will define the various methods needed for our service, such as the interface with our external dependency. By implementing a class that conforms to this protocol, we can customize the behavior based on our specific needs.
For example, in our ++code>TODO ++/code> application, we have an external dependency that enables us to perform API calls to retrieve tasks. To begin with, we create a protocol that defines the necessary methods for interacting with our API.
Afterward, we create a class in the app that conforms to this protocol and performs the actual action. In this case, we invoke the request to retrieve all tasks from a ++code>TODO ++/code> application.
Finally, we can inject this class by converting it to its protocol and then utilize it in the app wherever necessary.
During our tests, we just need to provide an implementation of this protocol with a different behavior that has the same exact method calls as during normal application use, and inject it before running our tests.
An example of such an implementation is the following gist, which returns an empty list of tasks:
Of course, we can achieve this implementation to perform additional actions, such as retrieving all calls to the methods of this service in an array.
We can therefore perform tests of a component while maintaining a global behavior similar to the actual execution of the app.
However, there is one issue: currently, our implementation of ++code>TodoAPIService++/code> only returns an empty list. How can we populate this return with custom data?
One might initially think that it is possible to create new objects by passing the attribute to be modified as a parameter to the constructor.
There are several issues with this approach:
For example, if we have an ++code>Author++/code> class and a test to check the behavior of a method that retrieves the author’s full name. When reading the test, attributes other than ++code>firstname++/code> and ++code>lastname++/code> are useless and make the test less clear and harder to understand:
To solve the problems raised previously, we can use helpers.
The purpose of these helpers would be to create objects with various values based on input parameters.
This approach works fine for objects that require minimal modification to their fields and is more suitable for generating a collection of objects.
However, if the goal is to create a single object with specific attributes, a better solution would be to use mock builders.
The concept is to have a class that includes the following elements:
Therefore, for our ++code>Task++/code> object, the corresponding builder would have the following format:
We can then utilize this builder in a factory helper or directly to create ++code>Task++/code> objects with customized values during unit testing, for instance:
Here is a table summarizing the strengths and limitations of each of the presented methods:
In summary, the use of mocks and fake data is an important aspect in developing effective unit tests for Swift applications. By implementing a strategy based on protocols and mock builders, it becomes possible to simplify data generation and maintain readable and maintainable tests. So feel free to explore these methods to enhance the quality of your code. Happy programming!