Testing the list of sync-ups

The SyncUpsList feature we have built so far isn’t very complex. In fact, it only has one primary action, that of deleting sync-ups from its collection. However, it can be good practice to test new features as you add them, and in the future we will have much more complicated behavior to test.

TestingListOfSyncUps.tutorial

Writing your first test

Let’s write a test to verify that when the onDelete action is sent that the corresponding sync-ups are indeed deleted. This will require us to become familiar with the testing tools in the library, primarily the TestStore.

The first step to writing a new test is to construct a TestStore, which is a testable runtime for your feature. It allows you to emulate a series of actions your user takes in your feature, while asserting on how state changes each step of the way, and asserting how effects execute and feed their data back into the system.

As written, this test would fail if we ran it. Go ahead. Try it!

To get a passing test we must provide a trailing closure that describes how the state changes. The closure is handed a single, mutable argument that represents the state before the action was sent. It is your job to mutate that argument to represent the state after the action was processed by the reducer.

That is the basics of writing a test for our feature. This test is quite simple because the logic in the feature is still quite simple. Soon the logic and behavior of the feature will become more complex, and then tests will start to give us more confidence that the feature works as we expect.

  1. Start by creating a new file in your test target called SyncUpsListTests.swift, and paste in some basic scaffolding for a new test to verify the deletion logic.

    SyncUpsListTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpsListTests {
      @Test
      func deletion() async {
    
      }
    }
    TestingListOfSyncUps-01-code-0001.swift
  2. Construct a TestStore much like you would a regular Store, by providing the initial state for the feature to begin in as well as the reducer you want to test. We’ll start the feature with on sync-up already in its array so that we have something to delete.

    SyncUpsListTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpsListTests {
      @Test
      func deletion() async {
        let store = TestStore(
          initialState: SyncUpsList.State(
            syncUps: [
              SyncUp(
                id: SyncUp.ID(),
                title: "Point-Free Morning Sync"
              )
            ]
          )
        ) {
          SyncUpsList()
        }
      }
    }
    TestingListOfSyncUps-01-code-0002.swift
  3. Send an action into the test store to emulate the user doing something. In this case we are going to send the onDelete action to simulate the user swiping on the row and tapping “Delete”. And we can provide an IndexSet that represents deleting the first row.

    SyncUpsListTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpsListTests {
      @Test
      func deletion() async {
        let store = TestStore(
          initialState: SyncUpsList.State(
            syncUps: [
              SyncUp(
                id: SyncUp.ID(),
                title: "Point-Free Morning Sync"
              )
            ]
          )
        ) {
          SyncUpsList()
        }
    
        await store.send(.onDelete([0]))
      }
    }
    TestingListOfSyncUps-01-code-0003.swift
  4. Provide a trailing closure to ComposableArchitecture/TestStore/send(_:assert:fileID:file:line:column:)-8f2pl that mutates the state to its final value. In particular, we expect the syncUps to be an empty array, and for no other changes to happen.

    SyncUpsListTests.swift
    import ComposableArchitecture
    import Testing
    
    @testable import SyncUps
    
    @MainActor
    struct SyncUpsListTests {
      @Test
      func deletion() async {
        let store = TestStore(
          initialState: SyncUpsList.State(
            syncUps: [
              SyncUp(
                id: SyncUp.ID(),
                title: "Point-Free Morning Sync"
              )
            ]
          )
        ) {
          SyncUpsList()
        }
    
        await store.send(.onDelete([0])) {
          $0.syncUps = []
        }
      }
    }
    TestingListOfSyncUps-01-code-0004.swift