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. 13.
  2. 17.
  3. 19.

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

    store and return 19 — Vitalii Malakhovskyi, BetterMe
  4. 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
  5. 28.

    and then use separate library to mock it 28 —

    Vitalii Malakhovskyi, BetterMe
  6. 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
  7. 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
  8. 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
  9. 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
  10. 35.

    used approaches - protocols - dependency injection - covered with

    integration tests (bdd) 35 — Vitalii Malakhovskyi, BetterMe
  11. 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
  12. 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
  13. 49.

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

    49 — Vitalii Malakhovskyi, BetterMe
  14. 50.

    fetch failed -> download succeed -> store failed //given /*

    stub fetch */ /* stub download */ /* stub store */ /* setup sut */ //when ... //then ... 50 — Vitalii Malakhovskyi, BetterMe
  15. 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
  16. 52.
  17. 53.

    app design is good if it's covered with tests 53

    — Vitalii Malakhovskyi, BetterMe
  18. 54.

    app design is good if it's easy to cover with

    tests 54 — Vitalii Malakhovskyi, BetterMe
  19. 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
  20. 60.

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

    6) 7) 60 — Vitalii Malakhovskyi, BetterMe
  21. 61.

    hard to maintain, hard to add new features, hard cover

    it with tests 61 — Vitalii Malakhovskyi, BetterMe
  22. 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
  23. 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
  24. 68.
  25. 69.

    the more ⬥ we have, the more code paths we

    need to test 69 — Vitalii Malakhovskyi, BetterMe
  26. 70.

    ̄ makes our code difficult to test, as they need

    mocks 70 — Vitalii Malakhovskyi, BetterMe
  27. 71.

    separate logic from effects > manage logic & manage side

    effects 71 — Vitalii Malakhovskyi, BetterMe
  28. 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
  29. 76.

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

    */ /* stub download */ /* stub store */ preparation of concrete state 76 — Vitalii Malakhovskyi, BetterMe
  30. 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
  31. 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
  32. 80.

    hard setup becomes trivial /* setup sut */ /* stub

    fetching */ /* stub downloading */ /* stub storing */ vs let state = State.downloading(url) 80 — Vitalii Malakhovskyi, BetterMe
  33. 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
  34. 84.

    side effects - dependency injection - frameworks - async behavior

    - protocols 84 — Vitalii Malakhovskyi, BetterMe
  35. 85.

    dependency injection - weak incapsulation instument 3-rd party framework -

    unwanted details async behavior - harder to test/ understand 85 — Vitalii Malakhovskyi, BetterMe
  36. 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
  37. 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
  38. 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
  39. 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
  40. 91.

    simple test - does not brake if implementation changes -

    breaks if logic changes in concrete place 91 — Vitalii Malakhovskyi, BetterMe
  41. 96.

    somebody needs to interpret state in terms of side effects

    96 — Vitalii Malakhovskyi, BetterMe
  42. 97.

    imperative shell - chaos - async - frameworks - dependency

    injection implementation details 97 — Vitalii Malakhovskyi, BetterMe
  43. 99.

    state effect idle nothing downloading download storing store failed nothing

    succeed nothing 99 — Vitalii Malakhovskyi, BetterMe
  44. 100.
  45. 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
  46. 102.

    to test observer we might need - mock - stub

    - spy - fake 102 — Vitalii Malakhovskyi, BetterMe
  47. 103.

    well isolated tests logic effect 1) ✅ 1) 2) ✅

    2) 3) ✅ 3) 4) ✅ 4) 5) ✅ 5) 6) ✅ 6) 7) ✅ 7) 103 — Vitalii Malakhovskyi, BetterMe
  48. 105.

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

    — Vitalii Malakhovskyi, BetterMe
  49. 107.

    missing part - store - owns state - mutates state

    (reduce function) - notifies observers about update 107 — Vitalii Malakhovskyi, BetterMe
  50. 109.

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

    multiple stores synchronization - side effects sync with logic 109 — Vitalii Malakhovskyi, BetterMe
  51. 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
  52. 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
  53. 113.

    logic & effects write code write tests maintain code maintain

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