Using Dependency Injection to Mock Network API Service in View Controller
Published at Jun 28, 2019
Dependency Injection is a software engineering technique that can be used to pass other object/service as a dependency to an object that will use the service. It’s sometime also called inversion of control, which means the object delegates the responsibility of constructing the dependencies to another object. It’s the D part of the SOLID design principles, dependency inversion principle.
Here are some of the advantages of using dependency injection in your code:
- Better separation of concerns between construction and the use of the service.
- Loose coupling between objects, as the client object only has reference to the services using Protocol/Interface that will be injected by the injector.
- Substitutability, which means the service can be substituted easily with other concrete implementation. For example, using Mock object to return stub data in integration tests.
How to use Dependency Injection
There are 3 approaches that we can use for implementing dependency injection in our class.
Using Initializer
With initializer, we declare all the parameter with the interface that will be injected into the class. We assign all the parameters to the instance properties. Using this approach we have the option to declare all the dependent instance properties as private if we want.
class GameListViewController: UIViewController { private let gameService: GameService init(gameService: GameService) {
self.gameService = gameService
}
}
Using Setter
Using Setter. With setter, we need to declare the dependent instance properties as internal or public (for shared framework). To inject the dependencies, we can just assign it via the setter.
class GameListViewController: UIViewController { var gameService: GameService!}let gameVC = GameListViewController()
gameVC.gameService = GameService()
Using Method
Here, we declare a method with all the required parameters just like the initializer method.
class GameListViewController: UIViewController { private var gameService: GameService! func set(_ gameService: GameService) {
self.gameService = gameService
}
}
The most common approaches being used are the injection via initializer and setter.
What we will Refactor
Next, let’s refactor a simple GameDB app that fetch collection of games using the IGDB API. To begin, clone the starter project in the GitHub repository at alfianlosari/GameDBiOS-DependencyInjection-Starter
To use the IGDB API, you need to register for the API key in IGDB website at IGDB: Video Game Database API
Put the API Key inside the IGDBWrapper initializer in the GameStore.swift file Try to build and run the app. It should be working perfectly, but let’s improve our app to the next level with dependency injection for better separation if concerns and testability. Here are the problems with this app:
- Using GameStore class directly to access singleton. Using singleton object is okay, but the View Controllers shouldn’t know about the type of the concrete class that has the singleton.
- Strong coupling between the GameStore class and View Controllers. This means View Controllers can’t substitute the GameStore with mock object in integration tests.
- The integration tests can’t be performed offline or stubbed with test data because the GameStore is calling the API with the network request directly.
Declaring Protocol for GameService
To begin our refactoring journey, we will create a protocol to represent the GameService API as a contract for the concrete type to implement. This protocol has several methods:
- Retrieve list of games for specific platform (PS4, Xbox One, Nintendo Switch).
- Retrieve single game metadata using identifier.
In this case, we can just copy the methods in the GameStore to use it in this protocol.
protocol GameService {
func fetchPopularGames(for platform: Platform, completion: @escaping (Result<[Game], Error>) -> Void)
func fetchGame(id: Int, completion: @escaping (Result<Game, Error>) -> Void)
}
Implement GameService Protocol in GameStore class
Next, we can just implement the GameService protocol for the GameStore class. We don’t need to do anything because it already implemented all the required method for the protocol!
class GameStore: GameService {
...
}
Implement Dependency Injection for GameService in GameListViewController
Let’s move on to the GameListViewController, here are the tasks that we’ll do:
- Using initializer as the dependency injection for the GameService and Platform properties. In this case for GameService , we need to declare a new instance property to store it. For storyboard based view controller, you need to use setter to inject dependency.
- Inside the initializer block, we can just assign all the parameter into the instance properties.
- Inside the loadGame method, we change the singleton invocation to the GameService instance property to fetch the games for the associated platform.
class GameListViewController: UICollectionViewController {
private let gameService: GameService
private let platform: Platform
init(gameService: GameService, platform: Platform) {
self.gameService = gameService
self.platform = platform
super.init(collectionViewLayout: UICollectionViewFlowLayout())
}
private func loadGame() {
gameService.fetchPopularGames(for: platform) { (result) in
switch result {
case .success(let games):
self.games = games
case .failure(let error):
print(error.localizedDescription)
}
}
}
// ...
}
Next, we need to update the initialization of GameListViewController in the AppDelegate . Here, we just pass the platform and the GameStore instance to the initializer parameter like so.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
let tabController = UITabBarController()
let gameService = GameStore.shared
let controllers = Platform.allCases.map { (platform) -> UIViewController in
let gameVC = GameListViewController(gameService: gameService, platform: platform)
gameVC.title = platform.description
gameVC.tabBarItem = UITabBarItem(title: platform.description, image: UIImage(named: platform.assetName), tag: platform.rawValue)
return UINavigationController(rootViewController: gameVC)
}
tabController.setViewControllers(controllers, animated: false)
window.rootViewController = tabController
window.makeKeyAndVisible()
self.window = window
return true
}
}
Implement Dependency Injection for GameService in GameDetailViewController
Let’s move on to the GameDetailViewController, here are the tasks that we’ll do, it will pretty similar to GameListViewController changes:
- Using initializer as the dependency injection for the GameService and id properties. In this case for GameService , we need to declare a new instance property to store it. For storyboard based view controller, you need to use setter to inject dependency.
- Inside the initializer block, we can just assign all the parameter into the instance properties.
- Inside the loadGame method, we change the singleton invocation to the GameService instance property to fetch the games for the associated platform. Also, we don’t need to unwrap the optional game id anymore!
class GameDetailViewController: UITableViewController {
private let id: Int
private let gameService: GameService
init(id: Int, gameService: GameService) {
self.id = id
self.gameService = gameService
super.init(style: .plain)
}
private func loadGame() {
gameService.fetchGame(id: id) {[weak self] (result) in
switch result {
case .success(let game):
self?.buildSections(with: game)
case .failure(let error):
print(error.localizedDescription)
}
}
}
// ...
}
Next, we need to update the initialization of GameDetailViewController in the GameListViewController . Here, we just pass the id and the GameStore instance to the initializer parameter like so.
class GameListViewController: UICollectionViewController {
// ...
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let game = games[indexPath.item]
let gameDetailVC = GameDetailViewController(id: game.id, gameService: gameService)
navigationController?.pushViewController(gameDetailVC, animated: true)
}
}
Create MockGameService for View Controller integration Test in XCTest
Next, create a new target and select iOS Unit Testing Bundle from the selection. This will create a new target for testing. To run the test suite, you can press Command+U .
Let’s create a MockGameService class for mocking game service in view controller integration tests later. Inside each method, we just return the stub hardcoded games for stub data.
@testable import DependencyInjection
import Foundation
class MockGameService: GameService {
var isFetchPopularGamesInvoked = false
var isFetchGameInvoked = false
static let stubGames = [
Game(id: 1, name: "Suikoden 7", storyline: "test", summary: "test", releaseDate: Date(), popularity: 8.0, rating: 8.0, coverURLString: "test", screenshotURLsString: ["test"], genres: ["test"], company: "test"),
Game(id: 2, name: "Kingdom Hearts 4", storyline: "test", summary: "test", releaseDate: Date(), popularity: 8.0, rating: 8.0, coverURLString: "test", screenshotURLsString: ["test"], genres: ["test"], company: "test"),
]
func fetchPopularGames(for platform: Platform, completion: @escaping (Result<[Game], Error>) -> Void) {
isFetchPopularGamesInvoked = true
completion(.success(MockGameService.stubGames))
}
func fetchGame(id: Int, completion: @escaping (Result<Game, Error>) -> Void) {
isFetchGameInvoked = true
completion(.success(MockGameService.stubGames[0]))
}
}
Create GameListViewController XCTestCase
Next, let’s create test cases for GameListViewController , create a new file for unit test called GameListViewControllerTests.swift . Inside the sut (system under test) will be the GameListViewController . In the setup method we just instantiate the object and also the MockGameService object then assign it to the instance properties.
The first test is to test whether the GameService is invoked in the viewDidLoad method of GameListViewController . We create a function test case called testFetchGamesIsInvokedInViewDidLoad . Inside we just trigger the invocation of viewDidLoad , and then check the MockGameService isFetchPopularGamesInvoked is set to true for the assertion.
The second test is to test whether the fetchPopularGames method invocation fills the collection view with stub data. We create a function called testFetchGamesReloadCollectionViewWithData . Inside, we trigger the invocation of viewDidLoad and assert the number of items in the collection view is not equal to 0 for the test to pass.
import UIKit
import XCTest
@testable import DependencyInjection
class GameListViewControllerTests: XCTestCase {
var sut: GameListViewController!
var mockGameService: MockGameService!
override func setUp() {
super.setUp()
mockGameService = MockGameService()
sut = GameListViewController(gameService: mockGameService, platform: .ps4)
}
func testFetchGamesIsInvokedInViewDidLoad() {
_ = sut.view
XCTAssertTrue(mockGameService.isFetchPopularGamesInvoked)
}
func testFetchGamesReloadCollectionViewWithData() {
_ = sut.view
XCTAssertTrue(sut.collectionView.numberOfItems(inSection: 0) != 0)
}
}
Try to build and run all the tests with Command+U to view all the green symbols which means all tests passed successfully 😋!. For challenge, try to create the test cases for GameDetailViewController 🥰 using the MockGameService!
You can clone the end project in the GitHub repository at alfianlosari/GameDBiOS-DependencyInjection-Completed
Conclusion
Dependency injection is a really important software design principle that really help us to write more loosely coupled code between modules that lead to improved maintainability and flexibility in our codebase as the complexity of our app grow.
We can also use a centralized container to build our dependencies, and then resolve the dependencies only we require them in our class. This is really helpful, if our app navigation hierarchy is quite deep as we only need to pass the container down. So let’s keep the lifelong learning goes on and write better code.