Unit Testing in Swift: Dependency Injection
One of the harder topics for me to understand in testing is definitely dependency injection. However, after writing a bunch of tests last year (hello 2015!), I think I finally have a handle on it, and would like to share what I learned.
I’m going to use a very basic Minions app as an example. The app just shows a tableView of minion names and images. To get set up, I have a few basic objects set to go:
The Setup
The Minion struct holds information about each Minion. To make it simple, each Minion has a unique name, so two Minions with the same are actually the same Minion (as specified by the Equatable protocol extension):
// Minion.swift struct Minion { let name: String } extension Minion: Equatable { } func == (lhs: Minion, rhs: Minion) -> Bool { return lhs.name == rhs.name }
The MinionService would be used to make an API call to get the Minion data in the real world (using AFNetworking or Alamofire). To keep it simple, let’s pretend this is done asynchronously… For this example, it just returns two hard-coded Minions, since this is not the main part of this demo:
// MinionService.swift class MinionService { func getTheMinions(completionHandler: ([Minion]) -> Void) { println("getting minions asynchronously") let result = [Minion(name: "Bob"), Minion(name: "Dave")] completionHandler(result) } }
Deciding What To Test
So now let’s get to the fun part! Here is the initial version of the ViewController:
// ViewController.swift import UIKit class ViewController: UITableViewController { var dataSource: [Minion]? override func viewDidLoad() { super.viewDidLoad() fetchMinions() } func fetchMinions() { let minionService = MinionService() minionService.getTheMinions { [unowned self](minions) -> Void in println("Show all the minions!") self.dataSource = minions self.tableView.reloadData() } } // MARK: UITableViewDataSource override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataSource?.count ?? 0 } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("minionCell") as UITableViewCell if let minion = dataSource?[indexPath.row] { cell.textLabel?.text = minion.name cell.imageView?.image = UIImage(named: minion.name) } return cell } }
The part I want to focus on testing is the fetchMinions
method. I want to make sure that:
fetchMinions
calls the MinionService instead of just generating some random array of MinionsfetchMinions
assigns the resulting array of Minions to the data source. In your own ViewController, you might want to test other side effects if there are any.
Dependency Injection
The way to test the two things mentioned above, we need to dependency inject our own MinionService. There are several ways to do this, but in this case I’m going to use Method Injection – just pass a MinionService to the fetchMinions
method, using a default value (yay Swift!), so I don’t actually have to pass anything else if I don’t want to (e.g. the way it is called in viewDidLoad()
will not change!):
class ViewController: UITableViewController { var dataSource: [Minion]? override func viewDidLoad() { super.viewDidLoad() fetchMinions() } func fetchMinions(minionService: MinionService = MinionService()) { minionService.getTheMinions { [unowned self](minions) -> Void in println("Show all the minions!") self.dataSource = minions self.tableView.reloadData() } } // truncated code }
Write the Tests
The first step is getting a reference to the viewController instance. Since my viewController loads from the Storyboard, I want to make sure to also load it via Storyboard, so it’s set up just like it would be in the real world. This is how you do that in Swift:
// ViewControllerTests.swift import UIKit import Foundation import XCTest class ViewControllerTests: XCTestCase { var viewController: ViewController! override func setUp() { let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType)) let navigationController = storyboard.instantiateInitialViewController() as UINavigationController viewController = navigationController.topViewController as ViewController UIApplication.sharedApplication().keyWindow!.rootViewController = viewController let _ = viewController.view }
The next step is to create our own MinionService that we can control. This is the hardest part of dependency injection! The nice thing is that our Fake MinionService can be a subclass of MinionService, so we can override the method we’re testing (and if that method signature changes, our code won’t compile!).
Again, we want to make sure that our MinionService gets called when the fetchMinions
method is called in the ViewController, so we’ll have to add a getTheMinionsWasCalled
variable to our Fake MinionService to track this.
We also need to add a result variable to our Fake MinionService so that our test can control the result passed back to the ViewController. In summary, dependency injection lets us have very fine-grained control of what information is passed and used by the viewController.
// ViewControllerTests.swift class ViewControllerTests: XCTestCase { class Fake_MinionService: MinionService { var getTheMinionsWasCalled = false var result = [Minion(name: "Bob"), Minion(name: "Dave")] override func getTheMinions(completionHandler: ([Minion]) -> Void) { getTheMinionsWasCalled = true completionHandler(result) } } // truncated code }
Now, this is the easy part. Just write the test!
// ViewControllerTests.swift class ViewControllerTests: XCTestCase { // truncated code func test_fetchMinions() { let fakeMinionService = Fake_MinionService() viewController.fetchMinions(minionService: fakeMinionService) // Test that the MinionService was actually called XCTAssertTrue(fakeMinionService.getTheMinionsWasCalled) // Test that our injected result was assigned // to the dataSource (the side effect) if let dataSource = viewController.dataSource { XCTAssertEqual(fakeMinionService.result, dataSource) } else { XCTFail("Data Source should not be nil!!!") } } }
You can view the full sourcecode on Github here. Happy testing!