Test-Driven iOS: Dependency Injection

Reading Time: 2 minutes

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.

di-java

Suppose our ExampleViewController required a service to make a network call:


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.


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:


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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.