Improving your Unit Test Mocks with Arrays (iOS & Swift)

Ben Gilroy
Kin + Carta Created

--

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:

Calculator struct with a multiply function

We might want to test that its multiply function returns the correct result by writing the following test:

Test function to test Calculator multiply function

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.

Example test with injected mock of AnalyticsService

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:

Tapping fetch message shows loading spinner and then “Hello, world!”

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:

fetchMessage() function in 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:

MessageView mock using Bool flags to keep track of called functions

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:

Tests for MessagePresenter using Bool flag mock

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.

fetchMessage() function in MessagePresenter with an unwanted bug where showLoading is accidentally called twice
Tapping fetch message shows loading spinner and then “Hello, world!” although loading spinner does not disappear

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.

MessageView mock using call counts to keep track of called functions

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.

Tests for MessagePresenter using call count mock

If we run these tests again with the introduced bug we get the following failure:

Test failure message showing XCTAssertEqual failed: (“2”) is not equal to (“1”)

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:

fetchMessage() function in MessagePresenter with an unwanted bug where showLoading and hideLoading are called in the wrong order

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!

Tapping fetch message shows no loading spinner and then “Hello, world!” and loading spinner together

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.

MessageView mock using an array of Event enum cases to keep track of called functions

Now in our tests, we can assert that the array of captured events is equal to the array of events that we expect.

Tests for MessagePresenter using captured events array mock

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.

Long and unreadable test failure message for captured events array mock

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.

Updated Event enum to have rawValue type of String and conform to CustomStringConvertible

Now when our tests fail, we get nice readable failure messages that look the same as the array of expected events in your tests:

Nice readable test failure message

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! 🚀

--

--