Back in October 2019, I was assigned the task of implementing Apple Sign-in, a new single sign-on solution released by Apple as part of the release of iOS 13. It wasn’t too complicated to implement. Although, testing this framework certainly comes with its own set of challenges. Let’s take a look at how I implemented it.
| |
In one single method call, we initialise the provider, create the request, inject the request into the controller and then call the performRequests method against the controller. That’s a lot of creation for one single method.
Dependency Injection
Ideally, to enable testability, we want to decouple the usage of an object from its creation. Dependency injection is an ideal candidate for that. Although, we do have a slight issue…
Typically, an object’s dependency is injected via the init method and the method of the dependency would be called by the object that owns it. The problem here is that, because of how the framework has been designed, the ASAuthorizationController needs to be re-instantiated every time we call the authenticate method with new requests.
An easy solution to solving this problem is method injection. Every time we call the authenticate method we can inject a new ASAuthorizationController with its requests and allow it to be replaced with a mock during tests. But this is not ideal as we lose the abstraction we’re trying to create.
The reason why this is a wrapper is to avoid the rest of the codebase knowing about the framework we’re trying to use. We want to keep its usage in one centralised place and not leak framework details.
Factory Method
This brings me to the factory method design pattern. We can use this pattern to instantiate the object for us. Let’s give it a go!
| |
As you can see, the responsibility of instantiating the ASAuthorizationController has moved to the factory. Now, the factory will create a new controller every time the authenticate method is invoked.
Let’s take a look at what it looks like in a dependency diagram.

We have now decoupled creation from usage, allowing us to replace the factory adopting the interface with a spy during tests to make sure we’re sending the correct request. Given that the interface only has one method, we can easily replace this with a closure to keep things simpler. So now we have…
| |
Protocol Based Mocking
Let’s look at implementing the ASAuthorizationControllerDelegate, an interface for providing information about the outcome of an authorization request.
Usually, when an object conforms to a delegate, we call the methods that the object has implemented as a result of adopting the protocol and test the logic within the method(s). Nevertheless, this is not so straight forward in this case.
For the didCompleteWithAuthorization method to be invoked, we need to mock an ASAuthorizationController and ASAuthorization object.
For the ASAuthorizationController, we can reuse the spy controller we created with the factory.
The ASAuthorization is problematic. When we call the init method, we get a compiler warning informing us that the init method is unavailable. Subclassing the object to override the init method is also forbidden.
So how can we work around this? Protocol-Based Mocking!
Protocol based mocking is a nifty trick in Swift that is used to mimic the desired interfaces we’d like to spy on.
In this case, it’s the ASAuthorization. Although, even if we mimic the interface, one of its properties, the ASAuthorizationCredential, needs to be cast to ASAuthorizationAppleIDCredential which also can’t be initialised, so we need to mock that one instead.
We create an extension of the object and make it conform to our interface, giving us more control of the implementation and allow for the ability of mocking that interface.
| |
We can then create a method that we can invoke, completeWith(credential:) which we can test. The method that we can’t invoke then just forwards the message to this method.
| |
Right now, the method that we have created doesn’t look too busy. There’s probably an argument to not write tests for this method at all as it only contains success and error cases. But this would be useful if we want to fetch more details from the ASAuthorizationAppleIDCredential.
Given that it only requires writing two tests, for now, there’s certainly no harm in writing them.
Alternatives
We could also take another route to increase coverage: UI Tests.
But this is onerous given that we don’t own the framework and the implementation could change at any point. Also, there are uncommon edge cases such as a user not being signed into their Apple ID.
Full Code
You can find the full GitHub project here.