Xcoding with Alfian

Software Development Videos & Tutorials

Using Dependency Injection to Mock Network API Service in View Controller

Alt text

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:

  1. Better separation of concerns between construction and the use of the service.
  1. Loose coupling between objects, as the client object only has reference to the services using Protocol/Interface that will be injected by the injector.
  1. 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:

  1. 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.
  1. Strong coupling between the GameStore class and View Controllers. This means View Controllers can’t substitute the GameStore with mock object in integration tests.
  1. 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:

  1. Retrieve list of games for specific platform (PS4, Xbox One, Nintendo Switch).
  2. 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:

  1. 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.
  2. Inside the initializer block, we can just assign all the parameter into the instance properties.
  3. 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:

  1. 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.
  2. Inside the initializer block, we can just assign all the parameter into the instance properties.
  3. 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.