Dependency Injection for testability of iOS app

6427501df5c44488da4cae07055897fe?s=47 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

6427501df5c44488da4cae07055897fe?s=128

Elvis Lin

October 21, 2018
Tweet

Transcript

  1. 如何使⽤用
 Dependency Injection 
 提⾼高 iOS App 的可測試性 Elvis Lin

    @iPlayground.io
 2018-10-21
  2. 關於我 • Elvis Lin • Android, iOS 與 React Native

    永遠的初學者 • Twitter: @elvismetaphor • Blog: https://blog.elvismetaphor.me
  3. ⼤大綱 • 測試是什什麼 • 單元測試是什什麼 • 為什什麼不容易易對某個類別/模組寫單元測試 • 什什麼是依賴注入( Dependency

    Injection ) • 怎麼做依賴注入
  4. 什什麼是測試 •確定開發完成的功能符合規格 • 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)
  5. 為什什麼發佈前要測試? 來來源:https://failblog.cheezburger.com/

  6. 測試⾦金金字塔 (1/2) 來來源:https://martinfowler.com/bliki/TestPyramid.html

  7. 測試⾦金金字塔 (2/2) •UI (End-to-End) Test 
 最慢,執⾏行行成本最⾼高 •Unit Test 的執⾏行行速度


    最快,成本最低 這個議程關注的測試類型
  8. 單元測試 ( Unit Testing ) • 程式中最⼩小的邏輯單元為⽬目標,撰寫測試程式,驗證程式 的邏輯是否符合預期 • 程式單元是應⽤用的最⼩小可測試部件,在程序化編程中,⼀一

    個單元就是單個函式,對於物件導向程式設計,最⼩小單元 就是⽅方法 • ⼜又稱模組測試 ( Module Testing )
  9. 單元測試 測試程式 被測試程式
 ( SUT ) Software under test 操作

    SUT 改變狀狀態/回傳結果 驗證
  10. ⼀一個簡單的單元測試 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 } }
  11. 真實世界的單元測試 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") }
  12. 被隱藏的真相 測試程式 SUT 依賴A 依賴B 依賴C 依賴的
 依賴X 依賴的
 依賴Y

  13. 依賴 ( dependency ) class RecipesService { let repository: RecipesRepository

    init(repository: RecipesRepository = YummyRecipesRepository()) { self.repository = repository } } RecipesService 的依賴 依賴:可以被另⼀一個物件使⽤用的物件
  14. SUT過多地依賴 • 明顯拉長測試執⾏行行的時間 • SUT 很難在測試時被正確的初始化 • 每次測試執⾏行行的結果可能都不同 • Ex:

    SUT 中⽤用到了了亂數產⽣生器
  15. 為什什麼有的類別不好測試? • 在類別內直接初始化成員變數 • let xxxManager = new xxxManager() •

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

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

    { … … } } • 這個物件的依賴的實作被固定了了,無法被輕易易的替換 • 這個物件除了了實作業務邏輯,還要處理理依賴的⽣生成
  18. 拒絕讓物件/類別過勞

  19. 拒絕讓物件/類別過勞 Single Responsibility

  20. 解決⽅方式 依賴物件的產⽣生 與初始化 主要的業務邏輯 把兩兩個不同的⼯工作
 交給不同的類別/物件負責

  21. 依賴注入 
 ( Dependency Injection ) Injection

  22. 什什麼是依賴注入 • ”In software engineering, dependency injection is a software

    design pattern that implements inversion of control for resolving dependencies." - Wikipedia
  23. 你給我翻譯翻譯! 來來源:《讓⼦子彈⾶飛》

  24. 什什麼是依賴注入(簡明版 ) • ”Dependency injection is really just passing in

    an instance variable." - James Shore
 
 ( 依賴注入是從物件外部傳入物件需要的「實例例變數/成員 變數/屬性/參參數」 )
  25. 為什什麼要有依賴注入 • 讓依賴的產⽣生與物件的主要邏輯分開 • 減少物件之間的耦合程度 ( loosely coupling ) •

    讓開發⼈人員更更潮
  26. 依賴注入的⽅方式 • ⼿手動操作 • Initializer injection • Property injection •

    Method injection • 使⽤用 library / framework • Dependency Injection (DI) Container
  27. Initializer Injection class RecipesService { private let repository: RecipesRepository init(repository:

    RecipesRepository) { self.repository = repository } }
  28. Initializer Injection • 透過初始化函數建構依賴 • 在物件初始化時,就可以知道有哪些依賴需要初始化 • 適合⽤用在物件的內部的依賴在物件整個⽣生命週期中不會改 變的情況 •

    依賴物件可以不⽤用公開/暴暴露給外部使⽤用者
  29. Property Injection class RecipesService { var repository: RecipesRepository? init() {

    … } } Class ViewModel { func initializeRecipesService() { let recipesService = RecipesService() recipesService.repository =
 YummyRecipesRepository() } }
  30. Property Injection • 當你的依賴有可能在執⾏行行期改變的時候 • 可以輕易易的抽換依賴物件的實作 • 對於物件的使⽤用者來來說比較不清晰,需要閱讀程式碼才知 道哪些依賴物件需要另外設定

  31. Method Injection class RecipesService { getRecipes(
 repository: RecipesRepository
 ) ->

    [Recipes] { return repository.getRecipes() } }
  32. Method Injection • 在呼叫物件的⽅方法時,才將物件需要的依賴傳入 • 可以輕易易地切換物件的實作 • 每次呼叫⽅方法時都需要傳入依賴

  33. 使⽤用 DI Container • 減少為了了執⾏行行注入⽽而撰寫的 Boilerplate code • 因為物件的⽣生成⽅方式是預先定義好的,讓產⽣生的物件可以 擁有⼀一樣的初始值

    • Swift 上⾯面常⾒見見的兩兩個 framework • Cleanse • Swinject
  34. Cleanse

  35. Swinject

  36. 使⽤用 Swinject 做 DI

  37. ⼀一個簡單的 sample ViewController ViewModel FileManager

  38. FileManager import Foundation protocol FileManager { func getFileInfo() -> String

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

    FileManager init(fileManager: FileManager) { self.fileManager = fileManager } func getFileInfo() { self.fileInfo = self.fileManager.getFileInfo() } }
  40. 在 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 }() }
  41. 在 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 }()
  42. 在 ViewController 中取得 ViewModel internal class ViewController: UIViewController { @IBOutlet

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

    viewModel = DefaultViewModel(fileManager: MockFileManager()) viewModel.getFileInfo() XCTAssertEqual(viewModel.fileInfo!, "mock") } }
  44. 依賴注入帶來來的好處 • 提⾼高透明度,讓類別的責任更更清楚 • 讓物件的主要業務邏輯可以凸顯出來來(不⽤用跟⽣生成依賴物 件的邏輯混在⼀一起) • 依賴物件可以被抽換,或者在物件初始化的時候⽤用其他的 實作取代 •

    降低物件與他的依賴物件 ( dependency ) 之間的耦合,每個 類別都可以獨立地修改實作
  45. 總結 • 單元測試難以攥寫,部分原因是因為被測試的程式 ( SUT ) 內部⾃自⾏行行初 始化了了依賴物件 ( dependency

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