In the last Test-Driven iOS post, we talked about setting up our code and test files to allow unit testing on storyboarded apps. I mentioned that an extension of that setup could be used to take control of dependency injection inside of our apps. We’ll talk about that today.
Suppose our ExampleViewController required a service to make a network call:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ExampleViewController: UIViewController { | |
var dependableService: DependableService | |
@IBOutlet var someTextLabel: UITextLabel! | |
class func loadFromStoryboard( | |
dependableService: DependableService) -> ExampleViewController { | |
let controller = UIStoryboard(name:"Main", bundle:NSBundle(forClass:self)) | |
.instantiateViewControllerWithIdentifier("ExampleViewController") | |
as! ExampleViewController | |
controller.dependableService = dependableService | |
return controller | |
} | |
required public init?(coder aDecoder: NSCoder) { | |
self.dependableService = AppEnvironment.sharedEnvironment.dependableService | |
super.init(coder:aDecoder) | |
} | |
} |
Here, we want to inject our DependableService. There are two methods featured above: the loadFromStoryboard() class method that we introduced last time and an initializer, which extends an initializer provided by the UIViewController class.
When the OS instantiates this view controller, it does so by using the initializer that you see above. This allows us to use it to set a field variable for our service from a shared environment that we set up for our app.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class AppEnviroment: NSObject { | |
static let sharedEnvironment = AppEnviroment() | |
let dependableService:DependableService | |
override init() { | |
self.dependableService = DependableService() | |
super.init() | |
} | |
} |
That same field variable is set to the parameter value we have added to loadFromStoryboard(). In our tests, we will call the loadFromStoryboard() method directly, so the initializer that gets our service from the shared environment is never hit. Instead, we can set up a fake version of our service like so:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ExampleViewControllerTest: XCTestCase { | |
class FakeDependableService: DependableService { | |
var lastRequestParameter: String? | |
var stubbedResponse: DependableResponse? | |
init() { | |
super.init() | |
self.lastRequestParameter = "" | |
self.stubbedResponse = nil | |
} | |
override func attemptCall(param: String?) -> Future<DependableResponse> { | |
self.lastRequestParameter = param | |
return Future(success: stubbedResponse!) | |
} | |
} | |
var dependableService = FakeDependableService() | |
func testClickCallButton_makesCallToService_successResponse() { | |
dependableService.stubbedResponse = DependableResponse.Success("You win!") | |
let controller = ExampleViewController.loadFromStoryboard(dependableService) | |
assertThat(controller.view, present()) | |
UIWindow.present(viewController: controller) { () in | |
controller.didTapCallButton(NSObject()) | |
assertThat(dependableService.lastRequestParameter, presentAnd(equalTo("Do I win?"))) | |
assertThat(controller.presentedViewController, presentAnd(instanceOf(NextViewController))) | |
} | |
} | |
} |
And then we can use it to assert that we interacted with that service the way that we expected.
It’s worth noting that Swinject can be used to handle dependency injection for you. I have found its setup to be no less cumbersome than writing a minimal solution like this one. That having been said, the framework may be more suitable if you have more complex dependency injection requirements.