Managed side effects

Managed side effects

Talk by Vitalii Malakhovskiy.

⁃ расскажу как мы пишем сложную логику, чтобы потом в этом еще и разобраться
⁃ снова про data-driven
⁃ как потом покрыть эту логику тестами
⁃ когда так делать не надо

This talk was made for CocoaHeads Kyiv #15 which took place Jul 28, 2019. (https://cocoaheads.org.ua/cocoaheadskyiv/15)

Video: https://youtu.be/bhEn-VOH0q0

Db84cf61fdada06b63f43f310b68b462?s=128

CocoaHeads Ukraine

July 28, 2019
Tweet

Transcript

  1. managed side effects 1 — Vitalii Malakhovskyi, BetterMe

  2. pain 2 — Vitalii Malakhovskyi, BetterMe

  3. sometimes tests makes everything even worth 3 — Vitalii Malakhovskyi,

    BetterMe
  4. how do we solve these problems 4 — Vitalii Malakhovskyi,

    BetterMe
  5. does it actually gives anything 5 — Vitalii Malakhovskyi, BetterMe

  6. managed side effects 6 — Vitalii Malakhovskyi, BetterMe

  7. malakhovskyi vitalii 7 — Vitalii Malakhovskyi, BetterMe

  8. 8 — Vitalii Malakhovskyi, BetterMe

  9. constantly updating, long term products 9 — Vitalii Malakhovskyi, BetterMe

  10. maintainability 10 — Vitalii Malakhovskyi, BetterMe

  11. ? 11 — Vitalii Malakhovskyi, BetterMe

  12. cheap code changes cheap tests changes 12 — Vitalii Malakhovskyi,

    BetterMe
  13. None
  14. UI data driven approach 14 — Vitalii Malakhovskyi, BetterMe

  15. chapter 1 problem 15 — Vitalii Malakhovskyi, BetterMe

  16. business logic 16 — Vitalii Malakhovskyi, BetterMe

  17. data processing rules according to business needs — wiki 17

    — Vitalii Malakhovskyi, BetterMe
  18. service manager facadeservice interactor 18 — Vitalii Malakhovskyi, BetterMe

  19. images service 1) fetch if already downloaded 2) download 3)

    store and return 19 — Vitalii Malakhovskyi, BetterMe
  20. measure component complexity by it's tests ! 20 — Vitalii

    Malakhovskyi, BetterMe
  21. looks simple - fetch success -> data - fetch fail

    -> download fail -> error - fetch fail -> download success -> store fail -> error - fetch fail -> download success -> store success -> data 21 — Vitalii Malakhovskyi, BetterMe
  22. unit test integration test 22 — Vitalii Malakhovskyi, BetterMe

  23. dependencies 23 — Vitalii Malakhovskyi, BetterMe

  24. mocks 24 — Vitalii Malakhovskyi, BetterMe

  25. verify that we properly calling dependencies 25 — Vitalii Malakhovskyi,

    BetterMe
  26. async behavior 26 — Vitalii Malakhovskyi, BetterMe

  27. keep up with youtube trends, use rx 27 — Vitalii

    Malakhovskyi, BetterMe
  28. and then use separate library to mock it 28 —

    Vitalii Malakhovskyi, BetterMe
  29. before - fetch success -> data - fetch fail ->

    download fail -> error - fetch fail -> download success -> store fail -> error - fetch fail -> download success -> store success -> data 29 — Vitalii Malakhovskyi, BetterMe
  30. after - verify m(fetch) - m(fetch) success -> data -

    m(fetch) fail -> verify m(download) - m(fetch) fail -> m(download) fail -> error - m(fetch) fail -> m(download) success -> verify m(store) - m(fetch) fail -> m(download) success -> m(store) fail -> error - m(fetch) fail -> m(download) success -> m(store) success -> data 30 — Vitalii Malakhovskyi, BetterMe
  31. bdd style tests describing behavior 31 — Vitalii Malakhovskyi, BetterMe

  32. describe("ImagesService") { context("when asking for image") { beforeEach { /*

    setup sut */ } it("should fetch data") { ... } context("but image is not cached") { beforeEach { /* stub fetching */ } it("should download image") { ... } context("and image download succeed") { beforeEach { /* stub downloading */ } it("should store data") { ... } } } } } 32 — Vitalii Malakhovskyi, BetterMe
  33. describe("ImagesService") { context("when asking for image") { beforeEach { /*

    setup sut */ } it("should fetch data") { ... } context("but image is not cached") { beforeEach { /* stub fetching */ } it("should download image") { ... } context("and image download succeed") { beforeEach { /* stub downloading */ } it("should store data") { ... } } } } } 33 — Vitalii Malakhovskyi, BetterMe
  34. everything looks fine 34 — Vitalii Malakhovskyi, BetterMe

  35. used approaches - protocols - dependency injection - covered with

    integration tests (bdd) 35 — Vitalii Malakhovskyi, BetterMe
  36. chapter 2 nabrasivaem na ventylator 36 — Vitalii Malakhovskyi, BetterMe

  37. 37 — Vitalii Malakhovskyi, BetterMe

  38. maintainability 38 — Vitalii Malakhovskyi, BetterMe

  39. 1 39 — Vitalii Malakhovskyi, BetterMe

  40. mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks

    mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks mocks 40 — Vitalii Malakhovskyi, BetterMe
  41. ! write each mock manually 41 — Vitalii Malakhovskyi, BetterMe

  42. ! use library to unify writing mocks 42 — Vitalii

    Malakhovskyi, BetterMe
  43. ! use codegen to generate mocks 43 — Vitalii Malakhovskyi,

    BetterMe
  44. 44 — Vitalii Malakhovskyi, BetterMe

  45. 2 45 — Vitalii Malakhovskyi, BetterMe

  46. complex tests 46 — Vitalii Malakhovskyi, BetterMe

  47. describe("ImagesService") { context("when asking for image") { beforeEach { /*

    setup sut */ } it("should fetch data") { ... } context("but image is not cached") { beforeEach { /* stub fetching */ } it("should download image") { ... } context("and image download succeed") { beforeEach { /* stub downloading */ } it("should store data") { ... } } } } } 47 — Vitalii Malakhovskyi, BetterMe
  48. quick - hides complexity 48 — Vitalii Malakhovskyi, BetterMe

  49. let's imagine how simple test will look like without quick

    49 — Vitalii Malakhovskyi, BetterMe
  50. fetch failed -> download succeed -> store failed //given /*

    stub fetch */ /* stub download */ /* stub store */ /* setup sut */ //when ... //then ... 50 — Vitalii Malakhovskyi, BetterMe
  51. func testPaymentQueue_when_oneTransactionForEachState_onePayment_noRestorePurchases_oneCompleteTransactions_then_correctCallbacksCalled() { // setup let spy = PaymentQueueSpy() let

    paymentQueueController = PaymentQueueController(paymentQueue: spy) let purchasedProductIdentifier = "com.SwiftyStoreKit.product1" let failedProductIdentifier = "com.SwiftyStoreKit.product2" let restoredProductIdentifier = "com.SwiftyStoreKit.product3" let deferredProductIdentifier = "com.SwiftyStoreKit.product4" let purchasingProductIdentifier = "com.SwiftyStoreKit.product5" let transactions = [ makeTestPaymentTransaction(productIdentifier: purchasedProductIdentifier, transactionState: .purchased), makeTestPaymentTransaction(productIdentifier: failedProductIdentifier, transactionState: .failed), makeTestPaymentTransaction(productIdentifier: restoredProductIdentifier, transactionState: .restored), makeTestPaymentTransaction(productIdentifier: deferredProductIdentifier, transactionState: .deferred), makeTestPaymentTransaction(productIdentifier: purchasingProductIdentifier, transactionState: .purchasing) ] var paymentCallbackCalled = false let testPayment = makeTestPayment(productIdentifier: purchasedProductIdentifier) { result in paymentCallbackCalled = true if case .purchased(let payment) = result { XCTAssertEqual(payment.productId, purchasedProductIdentifier) } else { XCTFail("expected purchased callback with product id") } } var completeTransactionsCallbackCalled = false let completeTransactions = CompleteTransactions(atomically: true) { payments in completeTransactionsCallbackCalled = true XCTAssertEqual(payments.count, 3) XCTAssertEqual(payments[0].productId, failedProductIdentifier) XCTAssertEqual(payments[1].productId, restoredProductIdentifier) XCTAssertEqual(payments[2].productId, deferredProductIdentifier) } // run paymentQueueController.completeTransactions(completeTransactions) paymentQueueController.startPayment(testPayment) paymentQueueController.paymentQueue(SKPaymentQueue(), updatedTransactions: transactions) paymentQueueController.paymentQueueRestoreCompletedTransactionsFinished(SKPaymentQueue()) // verify XCTAssertTrue(paymentCallbackCalled) XCTAssertTrue(completeTransactionsCallbackCalled) } 51 — Vitalii Malakhovskyi, BetterMe
  52. tests - heath indicator for your app design 52 —

    Vitalii Malakhovskyi, BetterMe
  53. app design is good if it's covered with tests 53

    — Vitalii Malakhovskyi, BetterMe
  54. app design is good if it's easy to cover with

    tests 54 — Vitalii Malakhovskyi, BetterMe
  55. 3 55 — Vitalii Malakhovskyi, BetterMe

  56. let's change something in downloading - verify m(fetch) - m(fetch)

    success -> data - m(fetch) fail -> verify m(download) - m(fetch) fail -> m(download) fail -> error - m(fetch) fail -> m(download) success -> verify m(store) - m(fetch) fail -> m(download) success -> m(store) fail -> error - m(fetch) fail -> m(download) success -> m(store) success -> data 56 — Vitalii Malakhovskyi, BetterMe
  57. !!!!!! 57 — Vitalii Malakhovskyi, BetterMe

  58. !!"!!! 58 — Vitalii Malakhovskyi, BetterMe

  59. !!"### 59 — Vitalii Malakhovskyi, BetterMe

  60. change some - brake all 1) 2) 3) 4) 5)

    6) 7) 60 — Vitalii Malakhovskyi, BetterMe
  61. hard to maintain, hard to add new features, hard cover

    it with tests 61 — Vitalii Malakhovskyi, BetterMe
  62. expensive 62 — Vitalii Malakhovskyi, BetterMe

  63. chapter 3 solution 63 — Vitalii Malakhovskyi, BetterMe

  64. what is the root of the problem? 64 — Vitalii

    Malakhovskyi, BetterMe
  65. do { let fetched = try read(url) completion(.success(fetched)) } catch

    { cancel = makeRequest(url).perform { [weak self] result in switch result { case .success(let value): ... case .failure(let error): ... } } } 65 — Vitalii Malakhovskyi, BetterMe
  66. do { let fetched = try read(url) //side effect completion(.success(fetched))

    //logic } catch { // logic, like if else //side effect cancel = makeRequest(url).perform { [weak self] result in switch result { case .success(let value): //logic ... case .failure(let error): //logic ... } } } 66 — Vitalii Malakhovskyi, BetterMe
  67. 67 — Vitalii Malakhovskyi, BetterMe

  68. ⬥ is responsible for the different code paths 68 —

    Vitalii Malakhovskyi, BetterMe
  69. the more ⬥ we have, the more code paths we

    need to test 69 — Vitalii Malakhovskyi, BetterMe
  70. ̄ makes our code difficult to test, as they need

    mocks 70 — Vitalii Malakhovskyi, BetterMe
  71. separate logic from effects > manage logic & manage side

    effects 71 — Vitalii Malakhovskyi, BetterMe
  72. functional core, imperative shell In fact, it dates back to

    2012, when Gary Bernhardt’s talked about it in Boundaries. 72 — Vitalii Malakhovskyi, BetterMe
  73. 73 — Vitalii Malakhovskyi, BetterMe

  74. ELM 74 — Vitalii Malakhovskyi, BetterMe

  75. logic 75 — Vitalii Malakhovskyi, BetterMe

  76. in previous chapter /* setup sut */ /* stub fetch

    */ /* stub download */ /* stub store */ preparation of concrete state 76 — Vitalii Malakhovskyi, BetterMe
  77. state 77 — Vitalii Malakhovskyi, BetterMe

  78. let's imagine our service as state machine enum State {

    case initial case checking(URL) case downloading(URL) case finished(URL, Data) case failed } 78 — Vitalii Malakhovskyi, BetterMe
  79. ⚠ state shouldn't have impossible state enum State { case

    initial case checking(URL) case downloading(URL) case finished(URL, Data) case failed(Error) } 79 — Vitalii Malakhovskyi, BetterMe
  80. hard setup becomes trivial /* setup sut */ /* stub

    fetching */ /* stub downloading */ /* stub storing */ vs let state = State.downloading(url) 80 — Vitalii Malakhovskyi, BetterMe
  81. state mutates with event 81 — Vitalii Malakhovskyi, BetterMe

  82. side effects does something, events describes what happens 82 —

    Vitalii Malakhovskyi, BetterMe
  83. side effects - all about implementation details do { let

    fetched = try read(url) completion(.success(fetched)) } catch { cancel = makeRequest(url).perform { [weak self] result in switch result { case .success(let value): ... case .failure(let error): ... } } 83 — Vitalii Malakhovskyi, BetterMe
  84. side effects - dependency injection - frameworks - async behavior

    - protocols 84 — Vitalii Malakhovskyi, BetterMe
  85. dependency injection - weak incapsulation instument 3-rd party framework -

    unwanted details async behavior - harder to test/ understand 85 — Vitalii Malakhovskyi, BetterMe
  86. event is data, data is boundaries - does not depends

    on frameworks - has no execution - data cannot be async - better interface 86 — Vitalii Malakhovskyi, BetterMe
  87. let's focus on what happens, not how enum Event {

    case fetch(URL) case fetchSucceed(Data) case fetchFailed(Error) case download(URL) case downloadSucceed(Data) case downloadFailed(Error) ... } 87 — Vitalii Malakhovskyi, BetterMe
  88. (state, event) -> state 88 — Vitalii Malakhovskyi, BetterMe

  89. function call or result handling becomes a data func reduce(state:

    State, event: Event) -> State { switch (state, event) { case (.downloading(let url), .downloadSucceed(let data)): return .finished(url, data) case (.downloading, .downloadFailed(let error)): return .failed(error) ... 89 — Vitalii Malakhovskyi, BetterMe
  90. real test let url = URL.random() let data = Data.random()

    let state = reduce(.downloading(url), with: .downloadSucceed(data)) XCTAssertEqual(state, .finished(url, data)) 90 — Vitalii Malakhovskyi, BetterMe
  91. simple test - does not brake if implementation changes -

    breaks if logic changes in concrete place 91 — Vitalii Malakhovskyi, BetterMe
  92. !!"!!! 92 — Vitalii Malakhovskyi, BetterMe

  93. !!"!!! 93 — Vitalii Malakhovskyi, BetterMe

  94. logic = state and events 94 — Vitalii Malakhovskyi, BetterMe

  95. what about effects? 95 — Vitalii Malakhovskyi, BetterMe

  96. somebody needs to interpret state in terms of side effects

    96 — Vitalii Malakhovskyi, BetterMe
  97. imperative shell - chaos - async - frameworks - dependency

    injection implementation details 97 — Vitalii Malakhovskyi, BetterMe
  98. observer listens for state updates, posts events 98 — Vitalii

    Malakhovskyi, BetterMe
  99. state effect idle nothing downloading download storing store failed nothing

    succeed nothing 99 — Vitalii Malakhovskyi, BetterMe
  100. state -> observer or state -> presenter -> controller 100

    — Vitalii Malakhovskyi, BetterMe
  101. class EffectsProducer { func handle(state: State, produce: @escaping (Event?) ->

    ()) { switch state { case .downloading(let url): makeRequest(url).perform { [weak self] data in if let data = try? data.get() { produce(.downloadSucceed(data)) } else { produce(.downloadFailed) } } ... 101 — Vitalii Malakhovskyi, BetterMe
  102. to test observer we might need - mock - stub

    - spy - fake 102 — Vitalii Malakhovskyi, BetterMe
  103. well isolated tests logic effect 1) ✅ 1) 2) ✅

    2) 3) ✅ 3) 4) ✅ 4) 5) ✅ 5) 6) ✅ 6) 7) ✅ 7) 103 — Vitalii Malakhovskyi, BetterMe
  104. not independent but has great resuability 104 — Vitalii Malakhovskyi,

    BetterMe
  105. whole picture -> state -> observer -> event -> 105

    — Vitalii Malakhovskyi, BetterMe
  106. unidirectional architecture 106 — Vitalii Malakhovskyi, BetterMe

  107. missing part - store - owns state - mutates state

    (reduce function) - notifies observers about update 107 — Vitalii Malakhovskyi, BetterMe
  108. chapter 4 conclusion 108 — Vitalii Malakhovskyi, BetterMe

  109. what is hard - integrate into non-data driven project -

    multiple stores synchronization - side effects sync with logic 109 — Vitalii Malakhovskyi, BetterMe
  110. don't do that if - in production project if you

    just heard about it - you have multiple stores with relations - your logic is simple 110 — Vitalii Malakhovskyi, BetterMe
  111. balance 111 — Vitalii Malakhovskyi, BetterMe

  112. to recap - found maintainability problem - manage side effects,

    manage logic - how to keep tests maintainable - how tests tell you when your code stinks 112 — Vitalii Malakhovskyi, BetterMe
  113. logic & effects write code write tests maintain code maintain

    tests mixed ✅ +/- ❌ ❌ separate +/- ✅ ✅ ✅ 113 — Vitalii Malakhovskyi, BetterMe
  114. 114 — Vitalii Malakhovskyi, BetterMe

  115. purpleshirted Vitalii Malakhovskyi VitaliyMal vmalakhovskiy 115 — Vitalii Malakhovskyi, BetterMe

  116. links 116 — Vitalii Malakhovskyi, BetterMe

  117. examples 117 — Vitalii Malakhovskyi, BetterMe

  118. q & a 118 — Vitalii Malakhovskyi, BetterMe