Testing Swift code that uses system singletons in 3 easy steps
Most apps written for any of Apple’s platforms rely on APIs that are singleton based. From UIScreen to UIApplication to NSDate, static APIs are everywhere in Foundation, UIKit & AppKit.
While singletons are very convenient and give easy access to a certain API from anywhere, they also pose a challenge when it comes to code decoupling and testing. Singletons are also a quite common source of bugs, where state ends up being shared and mutations not propagated properly throughout the system.
However, while we can refactor our own code to only use singletons where really needed, we can’t do much about what the system APIs give us. But the good news is, there are some techniques you can use to make your code that uses system singletons still be easy to manage & easy to test.
Let’s have a look at some code that uses the URLSession.shared singleton:
The above DataLoader is currently very hard to test, as it will automatically call the shared URL session and perform a network call. This would require us to add waiting and timeouts to our testing code, and it quickly becomes very tricky and unstable.
Instead, let’s go through 3 easy steps to make this code still as simple to use as currently, but making it a lot easier to test.
1. Abstract into a protocol
Our first task is to move the parts from URLSession that we need into a protocol that we can then easily mock in our tests. In my talk “Writing Swift code with great testability” I recommend avoiding mocks when possible, and while that’s a good strategy to follow for your own code, when interacting with system singletons — mocking becomes an essential tool to increase predictability.
Let’s create a NetworkEngine protocol and make URLSession conform to it:
As you can see above, we let URLSessionDataTask be an implementation detail of URLSession. That way, we avoid having to create multiple mocks in our tests, and can focus on the NetworkEngine API.
2. Use the protocol with the singleton as the default
Now, let’s update our DataLoader from before to use the new NetworkEngine protocol, and get it injected as a dependency. We’ll use URLSession.shared as the default argument, so that we can maintain backward compatibility and the same convenience as we had before.
By using a default argument, we can still easily create a DataLoader without having to supply a NetworkEngine — simply using DataLoader() — just like before.
3. Mock the protocol in your tests
Finally, let’s write a test — where we’ll mock NetworkEngine to make our test fast, predictable and easy to maintain.
Above you can see that I try to keep my mock as simple as possible. Instead of creating complicated mocks with lots of logic, it’s usually a good idea to just have them return some hardcoded value, that you can then make asserts against in your test. Otherwise, the risk is that you end up testing your mock more than you’re actually testing your production code.
We now have testable code, that still uses a system singleton for convenience — all by following these 3 easy steps:
- Abstract into a protocol
- Use the protocol with the singleton as the default
- Mock the protocol in your tests
Questions, feedback or comments are very welcome! Either post a response here on Medium, or contact me on Twitter @johnsundell.
Thanks for reading! 🚀