Improving the Testability of Apple Pay With Dependency Injection
We all know the importance of writing unit tests. But we also have seen many people who say that they take time to write, some things do not need to be tested, and all sorts of things that divide developer’s opinions as much as the endless debate ‘storyboards vs. programmatic views’.
But leaving personal preferences aside, I believe people develop some resistance to determined things because they might not understand or take time to understand those things. And it happens with me regarding automated testing.
The tests are useful to get fast confirmation that the code written works as expected and that when adding new features, nothing has been broken. Also, when working in a team, many different people might modify the project. So, once again, tests ensure the project’s integrity is maintained.
In the matter of unit tests, I am always intrigued, by testing the implementation of my code that makes use of third party dependencies. That’s the case of integrating Apple Pay, for example. That’s what I would like to talk about today.
The use cases for Apple Pay are pretty obvious. They very often involve the purchase of a product or service. And a wide range of apps are supporting it nowadays.
In order to make a payment using Apple Pay, there are some requirements. One of them is the creation of a PKPaymentRequest
(part of PassKit
framework), and the presentation of either a PKPaymentAuthorizationController
or a PKPaymentAuthorizationViewController
.
As Apple docs state:
The
PKPaymentAuthorizationController
class performs the same role as thePKPaymentAuthorizationViewController
class, but it does not depend on theUIKit
framework. This means that the authorization controller can be used in places where a view controller cannot (for example, inwatchOS
apps or inSiriKit
extensions).
In this example I’ll use PKPaymentAuthorizationViewController
, since the project will only use it with UIKit
for now. In the case of the example, the app will present the authorization view controller only if the user can make payments with one of the specified networks, provided by the method class func canMakePayments(usingNetworks: [PKPaymentNetwork]) -> Bool
.
Following this idea, we can then create a PaymentAuthorizationHandler
class, that creates the payment request, and completes with failure in case the networks are not supported. Like so:
When I try to unit test this behavior, I stumble upon the class function, which could be a barrier in the tests.
Dependency injection to the rescue
One way overcome this limitation, is to use dependency injection to determine the possibility to make the payments that the specific test needs. I could either:
- Use constructor injection and set it in the initializer.
- Use property injection.
When possible, I prefer to use constructor injection. So this is how this is how the PaymentAuthorizationHandler
would be:
The typealias is nothing more than a closure representing the class func canMakePayments(usingNetworks:)
signature. But with this change, it’s possible to pass, in production, the real method, and for test use, the value required for the test case can be provided by injecting it during the test setup. This also makes the test deterministic.
A test is deterministic, or repeatable, if it produces the very same output when given the same input no matter how many times it is run.
Now, in the test, the SUT (System Under Test) will have the value injected upon creation. This is how the test for the failure scenario looks like:
Breaking it down:
1 - The test needs to simulate a scenario where networks are not supported, so the sut
is injected with a closure that returns false
.
2 - An optional error is declared, so it can be captured in the requestAuthorization
method.
3 - The receivedError
is captured. In case an unexpected success message is received, the test simply fails with a message.
The next scenario to be covered is the case when the networks are supported, but the PKPaymentAuthorizationViewController
fails to be initialized.
The same approach will be used, but this time with the view controller initializer being injected via constructor injection. In case the initialization succeeds, the requestAuthorization
completes with success. This is the finished PaymentAuthorizationHandler
:
Now, the following is happening:
1 - The initializer signature is extracted into the typealias ApplePayControllerFactory
.
2 - An error case is declared to identify the cause of failure.
3 - The initializer now accepts a new parameter, which is the ApplePayControllerFactory
. In the case of production code, PKPaymentAuthorizationViewController.init
is used as default value.
4 - requestAuthorization
completes with the new error case failure, when the controller fails to be initialized.
5 - Finally the success case, the method completes with the initialized PKPaymentAuthorizationViewController
.
And the tests follow the same style of the first one. Except that this time, to trigger an initialization failure, an empty (invalid) instance of PKRequest
was injected, rather than a valid request, that was injected for the success case test.
So, let’s break it down.
1 - An invalid request is passed to the authorization controller.
2 - The request can be processed by the networks, by passing a closure that returns true
.
3 - There’s the assertion that when these conditions are presented, the failure output is .unableToInitialize
error.
4 - In the success scenario, a valid request is passed.
5 - The request can be processed by the networks, by passing a closure that returns true
.
6 - The test asserts that the receivedController is not nil
, as expected by the success case in the Result declared in PaymentAuthorizationHandler
’s ApplePayCompletionHandler
typealias.
There it goes. This is a small example of how it is possible to manipulate the input for the tests, for implementing Apple Pay. Of course, there’s much more to it. There’s the display of PKPaymentButton
,
the received messages from PKPaymentAuthorizationViewControllerDelegate
, the communication with a payment provider, and other specifications, depending on the use case.
And of course, there must be countless ways of achieving similar results, but this was the one I got to, and wanted to share. If you have seen something different, or thought of a different approach, you are welcome to share with me on twitter. 😄
The full project, with a sample UIViewController, displaying the button and performing the PKPaymentAuthorizationViewController
can be found on this repository.
References
Payment Service Provider - Apple
Apple Pay Tutorial - Getting Started - Ray Wenderlich