In order for objects to be easily testable, it is often good practice to use “Tell, don’t ask”. What this phrase means is that methods should not directly “ask” for (create) system objects that they need. Instead, they should be “told” which system object to use.
This really runs contrary to common iOS practice though. It is very common, even in Apple example code, to see objects creating system objects whenever and wherever needed.
For example, a class needing access to CoreLocation’s CLLocationManager object will typically insert the following statement wherever it is needed:
let locationManager = LocationManager()
This is quick and easy, but makes testing difficult. Unit test would probably want to ensure that calls to locationManager occur, either by testing for side effects, or else using a test double (mock) in place of a real LocationManager instance.
“Tell, Don’t Ask” would suggest that the class or method needing an instance of LocationManager be passed that instance. A negative effect of doing this though, is that the creator of the object or caller of the method needs to know about, and/or create the other object to pass. In this case, it would need to import CoreLocation.
// Create beacon class and determine if we're near it // caller code passes instance of CLLocationManager to init let myBeacon = MyBeaconClass(locationManager: CLLocationManager()) let isNear = myBeacon.isNear() // or passes it to specific methods that need it let myBeacon = MyBeaconClass() let isNear = myBeacon.isNear(locationManager:CLLocationManager())
Recently I have been working on cleaning up a reusable beacon class. I am trying to encapsulate it to the point that a user of the class would not need to know anything about CoreLocation. However, in the above example, the user would need to create an instance of LocationManager() to pass to the class’s initializer or to individual methods.
After pondering this a bit, I believe that in this case it works better to have locationManager defined as an instance variable (var instead of let) that is initialized by the beacon class initializers, but can then be overridden by the unit tests. This allows the unit tests to replace the default instance of LocationManager with a mock object, but does require the product code user to know anything about CLLocationManager. So this code would look like this:
// Create beacon class and determine if we're near it let myBeacon = MyBeaconClass() let isNear = myBeacon.isNear()
It is perfectly ok for the unit tests to have to know about CoreLocation, so the test code would look something like this:
// Create beacon class and determine if we're near it. let beaconUnderTest = MyBeaconClass() // Substitute a mock object beaconUnderTest.locationManager = MyMockLocationManager() let isNear = beaconUnderTest.isNear()
One big consideration when doing this is to keep the amount of work being done in the initializer small. If the instance of LocationManager is being manipulated during initialization, this manipulation may be difficult to test. So it may be necessary to provide separate methods to be called after initialization. The Single Responsibility Principle probably suggests doing this anyways.
“Tell, Don’t Ask” is still a recommended best practice. But it appears that in some cases using overridable instance variables provides better separation of concerns.