Native Technologies

Swift mocking and generating fake data: building effective and maintainable tests

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:

  • To create efficient and easy-to-maintain unit tests for your Swift application
  • To allow fixtures to be used while waiting to retrieve actual data.

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.

Mocking external dependencies

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?

Generating fake data in Swift

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:

  • As an application grows and objects become more complex, objects often have many attributes.
  • Passing all attributes as parameters can quickly make tests difficult to read and understand, as it becomes unclear which attributes are being tested.

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:

  • Tests become harder to maintain: if we add an attribute to an object, we have to modify all the tests that use that object. In the previous example, if we want to add a ++code>createdAt++/code> date field, we have to add it to every usage of the constructor.

To solve the problems raised previously, we can use helpers.

Factory 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.

Mock Builder

The concept is to have a class that includes the following elements:

  • All the attributes of the object that we want to create, initialized with default values.
  • Setters that enable modification of the object’s attributes.
  • A build method to generate the object with its attributes.

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:

Mocking method Pro Cons
Naive method
  • Easy to implement
  • Difficult to maintain
  • Requires refactoring whenever the object is modified
  • Becomes quickly unreadable
  • Becomes redundant if only one attribute needs to be modified
Factory helper
  • Facilitates the generation of a large volume of data
  • Not ideal for making simultaneous modifications to multiple attributes
Mock builder
  • Readable: we know what we are modifying
  • Limits the need for refactoring: adding an attribute simply involves adding an element with a default value in the mock builder
  • Requires some investment to implement


Conclusion

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!

Développeur mobile ?

Rejoins nos équipes