Mocking User Defaults in your Swift unit tests

Colin Wren

--

Photo by Sander Sammy on Unsplash

As I work on Garner, my career-orientated reflective journaling app I’ve moved from creating a rough proof-of-concept to adding a process of quality assurance into the way I approach building it.

As part of that process of building quality into my app I’ve moved from using SwiftData’s SwiftUI macros to my own version of the MVVM pattern in order to ensure I can unit test the system easier.

As I mentioned in my post about moving to that pattern, one benefit of encapsulating your app’s logic in a service class is that it allows you to use Dependency Injection which makes unit testing a lot easier.

Dependency Injection is a pattern where you explicitly pass instances of external code into the constructor of a class as this allows you to substitute those instances with test doubles when writing unit tests, giving you greater control to recreate the conditions that your code would run under in order to hit a target execution path.

One of the services I built in Garner used UserDefaults to store a set of values that are used in a form for setting the Reflection Reminder schedule. I decided to use UserDefaults for this as these values would be device specific and don’t need to be synced between devices but I want to be able to launch the app and restore those values to populate the form.

An example of a class that reads from UserDefaults on initialisation and saves a value in a method

In order to check that this restoration on launch worked I wanted to write some unit tests around my code’s interaction with UserDefaults :

  • That when there was no values saved under the keys that a set of default values were returned
  • That when there were values saved under the keys that those values were returned
  • That when updates were made as part of the form that those values were then saved to the same keys

To test this I would need to be able to have an instance of UserDefaults I can set values in before passing it into the service and be able to then read the values from after the service had changed the values stored under the key.

I needed to write a mock UserDefaults class.

Writing a mock UserDefaults

Fortunately UserDefaults has a pretty straightforward API for the operations I was using so I just needed to create a basic class that conformed to the UserDefaults protocol and implemented overrides for the set(_ value: Any?, forKey defaultName: String) and object(forKey defaultName: String) -> Any? functions (you may need other functions based on what methods you call to store your values).

In order to use the mock for testing the mock needs to be able to store the value(s) that would be stored via set() and then return those via object() as I’m only working with one value I added a basic variable to store these under but if you have multiple then a Dictionary would be a good data structure to use as it provides a key, value pair.

A mock of UserDefaults

Using a mock UserDefaults in tests

As the mock conforms to the UserDefaults protocol it can then be used in place of a real UserDefaults instance when creating an instance of the service under test.

You can call the set() function in the test to set the values that the service would restore on initialisation and call object() to read the values that would have been set.

Examples of using the mock to write values and read them

The benefit of this being a mock though is that it’s all in memory so is cleaned up at the end of the test, removing the possibility of values leaking between tests and impacting them.

A note on custom objects in UserDefaults

In my app I store a custom object in UserDefaults which isn’t stored the same way that simple values such as an Int or String would be stored. Custom objects are serialised and then stored as a String so your test code will need to be able to serialise and deserialise the custom object.

I encountered an issue when I first switched to a custom object as my mock was set to store the custom object in an non-serialised way and the tests worked but when running the app the app crashed because I wasn’t deserialising the object in the service itself.

The code I’ve used in my examples show the serialisation and deserialisation needed.

Summary

By creating a mock of UserDefaults I’ve been able to test the service that contains my app’s functionality easily and in a way that prevents potential test data leakage across test runs.

I’ve also learned how to create mocks for external services, something that will come in handy as I look to create a mock for UserNotifications in my next post.

--

--

Responses (3)