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.
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.
Start by creating a new SyncUpDetailTests.swift file and pasting some basic scaffolding for a new test.
SyncUpDetailTests.swift
TestingSyncUpDetail-01-code-0001.swiftimport ComposableArchitecture import Testing @testable import SyncUps @MainActor struct SyncUpDetailTests { @Test func edit() async { } }
Create a
TestStore
for theSyncUpDetail
feature.SyncUpDetailTests.swift
TestingSyncUpDetail-01-code-0002.swiftimport 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() } } }
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
TestingSyncUpDetail-01-code-0003.swiftimport 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)) } } }
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 abinding
action to set the sync-up.SyncUpDetailTests.swift
TestingSyncUpDetail-01-code-0004.swiftimport 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) { } } }
Assert how the state changes after sending the action. In particular, the
syncUp
data inside theedit
case of the destination should change.Run the test suite again to confirm that everything still passes.
SyncUpDetailTests.swift
TestingSyncUpDetail-01-code-0005.swiftimport 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 } } } }
Finish the user flow by emulating the user tapping on the “Done” button. We expect the
destination
state to benil
‘d out, which will cause the sheet to be dismissed. And we expect the parent feature’ssyncUp
state to be updated with the edited sync-up.Run the test suite to confirm it still passes.
SyncUpDetailTests.swift
TestingSyncUpDetail-01-code-0006.swiftimport 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 } } } }