Testing the sync-up detail

The SyncUpDetail feature has slowly become quite complex. It now handles two forms of navigation (an alert and sheet), it models navigation state with a single enum, and it updates the parent feature via a shared property. It’s about time we got some test coverage on this feature so we can be sure it works as we expect, and so that we can make future changes with confidence.

TestingSyncUpDetail.tutorial

Testing the edit flow

Let’s write a test for the edit flow. We will exercise the full user flow of tapping the “Edit” button, making some edits, and then committing the edits to the parent features.

We have now tested the full user flow of editing a sync-up, and because it passes we can be confident that there are no other state changes happening.

It is also possible to shorten this test quite a bit by using a non-exhaustive test store, as we did in Presenting the sync-up form, but we will leave that as an exercise for the reader.

We’ll stop here for now, but will get some coverage on the delete flow later on in Testing navigation.

  1. Start by creating a new SyncUpDetailTests.swift file and pasting some basic scaffolding for a new test.

    SyncUpDetailTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpDetailTests {
      @Test
      func edit() async {
      }
    }
    TestingSyncUpDetail-01-code-0001.swift
  2. Create a TestStore for the SyncUpDetail feature.

    SyncUpDetailTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpDetailTests {
      @Test
      func edit() async {
        let syncUp = SyncUp(
          id: SyncUp.ID(),
          title: "Point-Free Morning Sync"
        )
        let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) {
          SyncUpDetail()
        }
      }
    }
    TestingSyncUpDetail-01-code-0002.swift
  3. Emulate the user tapping on the “Edit” button and assert that the destination state mutates to point to the .edit case.

    Run the test suite to confirm that so far everything passes.

    SyncUpDetailTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpDetailTests {
      @Test
      func edit() async {
        let syncUp = SyncUp(
          id: SyncUp.ID(),
          title: "Point-Free Morning Sync"
        )
        let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) {
          SyncUpDetail()
        }
    
        await store.send(.editButtonTapped) {
          $0.destination = .edit(SyncUpForm.State(syncUp: syncUp))
        }
      }
    }
    TestingSyncUpDetail-01-code-0003.swift
  4. Emulate the user changing the title of the sync-up by sending a deeply nested action for the .destination, when it’s in the .edit case, and then finally a binding action to set the sync-up.

    SyncUpDetailTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpDetailTests {
      @Test
      func edit() async {
        let syncUp = SyncUp(
          id: SyncUp.ID(),
          title: "Point-Free Morning Sync"
        )
        let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) {
          SyncUpDetail()
        }
    
        await store.send(.editButtonTapped) {
          $0.destination = .edit(SyncUpForm.State(syncUp: syncUp))
        }
    
        var editedSyncUp = syncUp
        editedSyncUp.title = "Point-Free Evening Sync"
        await store.send(\.destination.edit.binding.syncUp, editedSyncUp) {
    
        }
      }
    }
    TestingSyncUpDetail-01-code-0004.swift
  5. Assert how the state changes after sending the action. In particular, the syncUp data inside the edit case of the destination should change.

    Run the test suite again to confirm that everything still passes.

    SyncUpDetailTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpDetailTests {
      @Test
      func edit() async {
        let syncUp = SyncUp(
          id: SyncUp.ID(),
          title: "Point-Free Morning Sync"
        )
        let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) {
          SyncUpDetail()
        }
    
        await store.send(.editButtonTapped) {
          $0.destination = .edit(SyncUpForm.State(syncUp: syncUp))
        }
    
        var editedSyncUp = syncUp
        editedSyncUp.title = "Point-Free Evening Sync"
        await store.send(\.destination.edit.binding.syncUp, editedSyncUp) {
          $0.destination?.modify(\.edit) { $0.syncUp = editedSyncUp }
        }
      }
    }
    TestingSyncUpDetail-01-code-0005.swift
  6. Finish the user flow by emulating the user tapping on the “Done” button. We expect the destination state to be nil‘d out, which will cause the sheet to be dismissed. And we expect the parent feature’s syncUp state to be updated with the edited sync-up.

    Run the test suite to confirm it still passes.

    SyncUpDetailTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpDetailTests {
      @Test
      func edit() async {
        let syncUp = SyncUp(
          id: SyncUp.ID(),
          title: "Point-Free Morning Sync"
        )
        let store = TestStore(initialState: SyncUpDetail.State(syncUp: Shared(value: syncUp))) {
          SyncUpDetail()
        }
    
        await store.send(.editButtonTapped) {
          $0.destination = .edit(SyncUpForm.State(syncUp: syncUp))
        }
    
        var editedSyncUp = syncUp
        editedSyncUp.title = "Point-Free Evening Sync"
        await store.send(\.destination.edit.binding.syncUp, editedSyncUp) {
          $0.destination?.modify(\.edit) { $0.syncUp = editedSyncUp }
        }
    
        await store.send(.doneEditingButtonTapped) {
          $0.destination = nil
          $0.$syncUp.withLock { $0 = editedSyncUp }
        }
      }
    }
    TestingSyncUpDetail-01-code-0006.swift