Improving your Unit Test Mocks with Arrays (iOS & Swift)
In this post, I’m going to show how you can leverage arrays in your mocks to write better quality and more readable unit tests that might help you catch bugs in your code that you didn’t even know existed.
What are unit tests and mocks?
Although I’m not going to cover in detail what mocks and unit tests are, it’s often useful to start with a quick zoom out.
Unit Testing
Unit testing is a way in which we can test the behaviour and functionality of individual chunks of code.
For example, let’s say we have a Calculator
object:
We might want to test that its multiply
function returns the correct result by writing the following test:
Mocks
In reality, we’re often dealing with objects that have a lot more complexity and are therefore more difficult to test. Some objects might even have dependencies such as a network service that makes API calls, or perhaps a storage service that saves data to the user’s device.
To test these objects, we can inject ‘mocks’ of these dependencies which we can control in our tests to make sure our object behaves as expected. For example, we might inject a mock URLSession
into a network service to ensure that the service maps to the correct success or failure result for various HTTP status codes that we tell our mock to return.
We can also use mocks to check that an object calls methods of other objects correctly. For example, we might have a view model that send an analytics event when a user signs out.
Now we’ve briefly covered what unit tests and mocks are, let’s jump into an example project where we can look at different ways we can write our mocks in unit tests to verify the behaviour of our code.
Our Example Project
In the example project for this post, you will find a single MessageViewController
following the Model-View-Presenter (MVP) architecture pattern with a button that says ‘Fetch message’.
Here’s what happens when you tap the button:
Let’s take a look at what is happening under the hood when we tap the fetch message button:
1. The MessageViewController
tells the MessagePresenter
that the fetch message button was tapped.
2. The MessagePresenter
then tells the view to show the loading state, and calls an asynchronous fetchMessage
function on the MessageService
.
3. The MessageService
then calls a completion handler with a Result<String, Error>
which provides the message if successful, or an error if it failed.
4. The MessagePresenter
then tells the view to update with fetched message (or error message), hide the loading state, and then show the message.
The main logic which handles the above is in the MessagePresenter
:
Let’s explore how we could test the above code to ensure everything is working as expected.
Testing using Bool flags
One way we can test the above function is to use a mock view which conforms to the MessageView
protocol with a series of Bool
flags that keep track of which functions are called. This might look something like this:
Then, in our tests we can set up our mock service with success or failure, call fetchMessageButtonTapped()
on the presenter, and assert that the correct functions get called, like so:
Great! Now we’ve tested all the correct functions get called and our test coverage is at 100% for our MessagePresenter
class.
However, test coverage does not necessarily indicate good test quality.
Let’s look at an example. If we add an extra call to view?.showLoading()
at the end of the fetchMessage()
function (as below) then our tests still pass, but now the loading indicator is still showing even after the message has been shown which is not what we want.
In order to catch this type of bug in our unit tests, let’s take a look at how we can use call counts in our mocks.
Testing using call counts
To make sure that each function is only called the correct number of times, we can update our mock to use call counters which will increment each time the function is called.
We can then update our tests to check the call count instead of checking that a bool flag has been set to true. This means if a function is called too many times it will fail the test.
If we run these tests again with the introduced bug we get the following failure:
Even though our test coverage is still 100%, we’ve now improved the quality of our unit tests by updating our mock view to check how many times our functions are called.
However, we can still take this one step further. Let’s look at another example:
Can you spot the bugs in the above code? We’ve accidentally called hideLoading()
and showLoading()
the wrong way round. Now our loading spinner doesn’t show during loading, and instead shows when the message shows — oops!
Even though we’ve introduced these bugs into our code, our updated unit tests will still pass because both hideLoading()
and showLoading()
are still being called the correct number of times (1) but they’re being called in the wrong order. Finally, let’s look at how we can update our mock and unit tests to also check the order in which the functions are called.
Testing using an Array of captured events
If you’re writing unit tests then you’re probably familiar with one of Swift’s primary collection types — array. An array is an ordered collection of values, so it seems like the perfect tool for the job of storing the order in which our mock’s functions are called.
Let’s update our mock by creating an enum where each case maps to an ‘event’ that our mock view should receive, such as showing the loading indicator. Then each time one of our mock view functions is called, we can ‘capture’ this event by adding the relevant case to an array of captured events.
Now in our tests, we can assert that the array of captured events is equal to the array of events that we expect.
If we run the tests now with showLoading()
and hideLoading()
the wrong way round, the tests fail — woo! This means our unit tests are now not only testing if each function was called, but also how many times it was called, and the order in which they are called together.
One other benefit (in my opinion anyway) is that using a combination of array and enum in our mock makes our test code much more readable than the previous examples.
Improving the failure message
However, one thing you may notice with the above example is that our failure messages have become quite hard to read.
We can improve this is by adding raw value type of String
to our Event
enum. Then, we can make the Event
enum conform to the CustomStringConvertible
protocol which means we can provide a custom description for each case of Event
by using the rawValue
property.
Now when our tests fail, we get nice readable failure messages that look the same as the array of expected events in your tests:
Wrapping up
That’s it! We’ve covered a few different ways in which we can write mocks for our unit tests, and learned how we can improve our test quality by using an array of captured events.
If you have any comments or questions, or just want to say hi – feel free to follow me on Twitter @GilroyBen 👋
Thanks for reading! 🚀