“Am I suggesting 100% test coverage? No, I’m not suggesting it. I’m demanding it. Every single line of code that you write should be tested. Period.”
― Uncle Bob
I have already written a post on this topic, but after discussing with several colleagues I have realized that the approach I was talking about might not be scalable. The approach discussed there relied on several hacks like method swizzling which though cool could cause some unexpected behaviour. We were relying on the internal implementation of UIViewControllers which might change anytime when devs at Apple feel like. Plus that approach made us comfortable in writing core business logic inside UIViewControllers instead of creating a ViewModels for them.
The approach I am going to describe here does not necessarily apply only to UIViewControllers. It can in fact be applied to any MVC pattern implementation. The problem with the current iOS implementation is that the UIViewController is very tightly coupled with the UIView. So if you want to test a your logic when a particular UIButton is tapped you cannot do that without inflating the whole view hierarchy.
The idea is to separate the business logic from the UIViewController to something like BusinessLogicController. The BusinessLogicController should have no reference of UIKit, thus making the code modular and platform independent. Technically speaking, this means that we aren’t testing the UIViewControllers, instead we are moving the core business out of it into another layer.
The UIViewController implements the Controllable protocol which basically has a set of methods -
- render : Called to paint something on the screen
- getValue: Called to get the value for a particular key
- showAlert: Optional, called to show an Alert
- goToPage: Optional, called to navigate to another page
- property eventable: To dispatchEvents to the BusinessLogicController
The first 4 methods are called by the BusinessLogicController as and when required. The UIViewController calls the dispatchEvent when a events like button clicked occur to let the BusinessLogicController know about a particular event.
The BusinessLogicController implements Eventable which has the following:
- dispatchEvent: called when a particular event like buttonPressed occurs
- property controllable: To call methods on the controllable whenever need be
Lets take the example of Login. Lets say you have to code the following scenario -
- When the username & password are correctly entered take the user to HomeVC.
- If the credentials entered are incorrect display the message “Incorrect username or password”.
- Also, if the user exceeds 5 attempts display an alert saying “Maximum number of retries exceeded”.
With the above criteria the LoginBusinessLogicController would look something like this -
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
Notice the it conforms to the Eventable protocol, which declares the method dispatchEvent.
To test such BusinessLogicControllers, you can create a StubbedControllable and use that to send events. I am not going to show the implementation of the StubbedControllable here because it will just bloat the code, if you really want to look at that you can head on my Github example repo. Lets look at how we could implement tests for this -
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
Note that neither the LoginBusinessLogicController nor its test is importing the UIKit module. The code written here is independent of the iOS UIKit Framework which means the tests can run as a separately from the Simulator, faster in-parallel if need be. As a bonus you can create a new iPad-App or Mac-App with the same BusinessLogicControllers wiring them with new ViewControllers.
I am pretty sure that by now you already have an idea of the LoginViewController’s implementation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|