Injecting and Mocking static frameworks in Swift

Cyril Le Pottier
Oct 23, 2019 · 5 min read

Unit testing is all about testing individual units of code in an isolated context. It relies on mocking and injecting their dependencies. It’s quite simple given the right code architecture (see Single Responsibility and Dependency Inversion principles for more), however it can become slightly harder when the dependencies are third-party frameworks. This is especially true in Swift where many of them are static, final or have instantiation restrictions. They are unfriendly to unit testing because they can’t easily be injected nor mocked. Fortunately, and this is the subject of this article, there is a technique to inject and mock such frameworks in Swift.

In this article, I will first take some times to explain our unit testing philosophy, here at OpenClassrooms, because we really value it. If you just want to read code, no worries, you can jump straight onto the second part where I will describe the technique to inject and mock static frameworks.

Our unit testing philosophy

At OpenClassrooms, we strongly believe that everything but views and controllers should be unit tested. Therefore, one on the very first tasks of the iOS project was to add a basic yet powerful rule to our Continuous Integration workflow: you can’t merge if the code coverage is not 100%.

Although I agree that not every single line of code is worth being tested, I still value the 100% code coverage rule as it guarantees that a decent amount of unit tests have been written for any given change. This rule is one of the reasons we can confidently release a new version of the OpenClassrooms app every two weeks.

This is all perfect! Or almost… The iOS community doesn’t strongly support unit testing as much as we would like. We regularly use frameworks that don’t support unit testing… They are a problem to us since they create untestable code that we have to, pardon my French, ignore from the code coverage calculation… 😨 Fortunately, we rarely have to take such action, because we have a way to unit test static, final or other singleton based frameworks.

Testing code based on static dependencies

Let’s focus on the most common of the unfriendly frameworks: the static ones. It generally comes with this kind of classes:

And you generally want to use it in your services:

The above service can unfortunately not be unit tested… Let’s fix that.

Injecting static classes

Our first step is injection. The tricky part is that we have to inject a class, and not an instance of it.

The good news is that static classes can be injected and this is all thanks to the Swift metatype type. This is a very interesting type and a lot can be said about it but I will try to keep it short and simple. Metatype is a type, like any type, it can be declared as a variable which will then refer to it. This is exactly what we need to inject our static class, so let’s use it!

We will upgrade our untestable service with the power of the metatype type. We will use the .Type keyword in order to declare a variable that will refer to the metatype type of the static class. We will then use the variable to access the static method, like if it was an instance method.

We will now use a second keyword, .self, in order to inject the metatype value to our service.

And here we are already! We injected our static framework.

Mocking static classes

Our second and last step is to mock our static class, it’s straightforward.

The compiler reminds us that we cannot override static method, great, this article would lost a lot of its purpose if we could. We can fortunately workaround this constraint by creating a protocol that will act as an overridable bridge.

We can now come back to our mock that couldn’t override static methods. Instead of subclassing the static lib, it will now implement the bridge protocol including the static method.

It looks good, doesn’t it? But we are not quite done here since our mock doesn’t have the same type than our static class, we cannot inject it to our service.

Easy fix, we will update again our now-almost-testable service to use the bridge we created earlier instead of the static class.

Thanks to this change we can now inject our mock in our service 😎 But because of this change we can no longer inject our static class in our service, oops… 😤 No worries, our library naturally complies to our bridge, we just have to let the Swift compiler know.

Note that this is important for the methods of the bridge to be exactly the same that the static class so the latter can naturally comply to the former. It if wasn’t the case, untestable mapping code would have to be written, which is ultimately moving the original problem elsewhere.

Wrap-up

The code now compiles. There is an obvious last step, someone has to write tests to bring back that code coverage to 100%! We won’t do that here, the scope of the article was to bring the code back to a testable state, which it did!

I would like to point out that the exact same technique works for classes that:

  • can’t be overriden, the most common example are final and non-open (aka public) classes.
  • can’t be instantiated, the most common example is Singletons with private initializers. We also have UIApplication which likes to remind you that “There can only be one UIApplication instance”.

I created an example code available on GitHub for the purpose of this article. It contains the code shown here with some extras: a mocking of a final class and of UIApplication.open(url:).

For the ones that want a “next step”, here is a challenge: Inject and mock Data.write(toFile:options:). Data is not overridable, it’s a struct, and it applies the write function on itself (not on an argument) which is next level trouble… A little secret, in the OpenClassrooms codebase, it is ignored from code coverage calculation. 🤫

Tech@OpenClassrooms

Tech@OpenClassrooms

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store