Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dependency Injection for testability of iOS app

Elvis Lin
October 21, 2018

Dependency Injection for testability of iOS app

Introduce how to use dependency injection to improve the testability of iOS app

Elvis Lin

October 21, 2018
Tweet

More Decks by Elvis Lin

Other Decks in Programming

Transcript

  1. 關於我 • Elvis Lin • Android, iOS 與 React Native

    永遠的初學者 • Twitter: @elvismetaphor • Blog: https://blog.elvismetaphor.me
  2. 什什麼是測試 •確定開發完成的功能符合規格 • responds correctly to all kinds of inputs,

    • performs its functions within an acceptable time, • is sufficiently usable, • can be installed and run in its intended environments, and • achieves the general result its stakeholders desire. (來來源: Wikipedia)
  3. ⼀一個簡單的單元測試 import XCTest class DummyTests: XCTestCase { override func setUp()

    { super.setUp() } override func tearDown() { super.tearDown() } // MARK: - Tests func testAddTwoNumber() { let result = addTwoNumber(left: 1, right: 2) XCTAssertEqual(result, 3) } 
 // MARK: - SUT func addTwoNumber(left: Int, right: Int) -> Int { return left + right } }
  4. 真實世界的單元測試 func test_UpdateSearchResults_ParsesData() { // given let promise = expectation(description:

    "Status code: 200") // when XCTAssertEqual(controllerUnderTest?.searchResults.count,
 0, "searchResults should be empty before the data task runs") let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) { data, response, error in if let error = error { print(error.localizedDescription) } else if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { promise.fulfill() self.controllerUnderTest?.updateSearchResults(data) } } } dataTask?.resume() waitForExpectations(timeout: 5, handler: nil) // then XCTAssertEqual(controllerUnderTest?.searchResults.count,
 3, "Didn't parse 3 items from fake response") }
  5. 依賴 ( dependency ) class RecipesService { let repository: RecipesRepository

    init(repository: RecipesRepository = YummyRecipesRepository()) { self.repository = repository } } RecipesService 的依賴 依賴:可以被另⼀一個物件使⽤用的物件
  6. 為什什麼有的類別不好測試? • 在類別內直接初始化成員變數 • let xxxManager = new xxxManager() •

    在建構⼦子 ( constructor ) 內做了了太多⼯工作 • 操作了了全域的狀狀態 ( Global State ) • 違反了了迪米特法則 ( Law of Demeter )
  7. 為什什麼有的類別不好測試? • 在類別內直接初始化成員變數 •let xxxManager = new xxxManager() • 在建構⼦子

    ( constructor ) 內做了了太多⼯工作 • 操作了了全域的狀狀態 ( Global State ) • 違反了了迪米特法則 ( Law of Demeter ) 這個議程要解決的問題
  8. 在類別內直接初始化成員變數 class RecipesService { let repository: RecipesRepository = YummyRecipesRepository() init()

    { … … } } • 這個物件的依賴的實作被固定了了,無法被輕易易的替換 • 這個物件除了了實作業務邏輯,還要處理理依賴的⽣生成
  9. 什什麼是依賴注入 • ”In software engineering, dependency injection is a software

    design pattern that implements inversion of control for resolving dependencies." - Wikipedia
  10. 什什麼是依賴注入(簡明版 ) • ”Dependency injection is really just passing in

    an instance variable." - James Shore
 
 ( 依賴注入是從物件外部傳入物件需要的「實例例變數/成員 變數/屬性/參參數」 )
  11. 依賴注入的⽅方式 • ⼿手動操作 • Initializer injection • Property injection •

    Method injection • 使⽤用 library / framework • Dependency Injection (DI) Container
  12. Property Injection class RecipesService { var repository: RecipesRepository? init() {

    … } } Class ViewModel { func initializeRecipesService() { let recipesService = RecipesService() recipesService.repository =
 YummyRecipesRepository() } }
  13. FileManager import Foundation protocol FileManager { func getFileInfo() -> String

    } public struct DefaultFileManager : FileManager { init() {} func getFileInfo() -> String { return "File Information" } }
  14. ViewModel public class DefaultViewModel: ViewModel { …… private let fileManager:

    FileManager init(fileManager: FileManager) { self.fileManager = fileManager } func getFileInfo() { self.fileInfo = self.fileManager.getFileInfo() } }
  15. 在 AppDelegate 中初始化 (1/2) @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow? let container: Container = { let container = Container() container.register(DefaultFileManager.self) { _ in
 DefaultFileManager.self()
 } container.register(DefaultViewModel) { resolver in let fileManager = resolver.resolve(DefaultFileManager.self)! return ViewModel(fileManager: fileManager) } return container }() }
  16. 在 AppDelegate 中初始化 (2/2) let container: Container = { let

    container = Container() container.register(DefaultFileManager.self) { _ in 
 DefaultFileManager.self()
 } container.register(DefaultViewModel.self) { resolver in let fileManager = resolver.resolve(DefaultFileManager.self)! return ViewModel(fileManager: fileManager) } return container }()
  17. 在 ViewController 中取得 ViewModel internal class ViewController: UIViewController { @IBOutlet

    weak private var primaryInfo: UILabel! let viewModel = (UIApplication.shared.delegate as! AppDelegate). container.resolve(DefaultViewModel.self)! …… }
  18. 如何測試 ViewModel class ViewModelTests: XCTestCase { func testGetFileInfo() { let

    viewModel = DefaultViewModel(fileManager: MockFileManager()) viewModel.getFileInfo() XCTAssertEqual(viewModel.fileInfo!, "mock") } }
  19. 總結 • 單元測試難以攥寫,部分原因是因為被測試的程式 ( SUT ) 內部⾃自⾏行行初 始化了了依賴物件 ( dependency

    ) • 透過依賴注入我們可以把⽣生成依賴物件的⾏行行為交給外部的物件實作 • 依賴注入 ( dependency injection ) 分成⼿手動注入跟使⽤用 DI Container 注 入兩兩⼤大類 • 透過 DI Container 可以簡化依賴注入的程式碼與保持依賴物件被初始化 時的⼀一致性 • 透過依賴注入,我們可以在測試時使⽤用 mock / stub 版本的依 賴物件,提升測試的執⾏行行速度,與降低測試撰寫的難度
  20. 參參考資料 • Design Tech Talk Series Presents: OO Design for

    Testability
 https://youtu.be/acjvKJiOvXw • Dependency Injection in Swift 
 https://youtu.be/-n8allUvhw8 • Dependency Injection (DI) in Swift
 http://ilya.puchka.me/dependency-injection-in-swift/