Skip to main content
A TestStore aids in writing expressive and exhaustive tests for features built in the Composable Architecture. It allows you to send a sequence of actions to the store, and each step of the way you must assert exactly how state changed, and how effect emissions were fed back into the system.

Class Definition

@MainActor
public final class TestStore<State: Equatable, Action>

Initialization

init(initialState:reducer:withDependencies:)
initializer
Creates a test store with an initial state and a reducer powering its runtime.Parameters:
  • initialState: The state the feature starts in
  • reducer: The reducer that powers the runtime of the feature
  • prepareDependencies: A closure that can be used to override dependencies that will be accessed during the test
  • fileID: The fileID
  • filePath: The filePath
  • line: The line
  • column: The column
public init(
  initialState: @autoclosure () -> State,
  reducer: () -> some Reducer<State, Action>,
  withDependencies prepareDependencies: (inout DependencyValues) -> Void = { _ in },
  fileID: StaticString = #fileID,
  file filePath: StaticString = #filePath,
  line: UInt = #line,
  column: UInt = #column
)

Properties

state
State
The current state of the test store.When read from a trailing closure assertion in send or receive, it will equal the inout state passed to the closure.
dependencies
DependencyValues
The current dependencies of the test store.The dependencies define the execution context that your feature runs in. They can be modified throughout the test store’s lifecycle in order to influence how your feature produces effects.
exhaustivity
Exhaustivity
The current exhaustivity level of the test store.Defaults to .on. Can be set to .off or .off(showSkippedAssertions: true) for non-exhaustive testing.
timeout
Duration
The default timeout used in all methods that take an optional timeout.This is the default timeout used in methods like receive and finish.

Methods

Sending Actions

send(_:assert:)
func
Sends an action to the store and asserts when state changes.To assert on how state changes you can provide a trailing closure, and that closure is handed a mutable variable that represents the feature’s state before the action was sent. You need to mutate that variable so that it is equal to the feature’s state after the action is sent.Parameters:
  • action: An action
  • updateStateToExpectedResult: A closure that asserts state changed by sending the action to the store. The mutable state sent to this closure must be modified to match the state of the store after processing the given action. Do not provide a closure if no change is expected.
Returns: A TestStoreTask that represents the lifecycle of the effect executed when sending the action.
@discardableResult
public func send(
  _ action: Action,
  assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil
) async -> TestStoreTask

Receiving Actions

receive(_:timeout:assert:)
func
Asserts an action was received from an effect and asserts how the state changes.When an effect is executed in your feature and sends an action back into the system, you can use this method to assert that fact, and further assert how state changes after the effect action is received.Parameters:
  • expectedAction: An action expected from an effect
  • duration: The amount of time to wait for the expected action
  • updateStateToExpectedResult: A closure that asserts state changed by sending the action to the store
public func receive(
  _ expectedAction: Action,
  timeout duration: Duration? = nil,
  assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil
) async where Action: Equatable

Finishing Tests

finish(timeout:)
func
Suspends until all in-flight effects have finished, or until it times out.Can be used to assert that all effects have finished.Parameters:
  • duration: The amount of time to wait before asserting
public func finish(
  timeout duration: Duration? = nil
) async

State Assertions

assert(_:)
func
Assert against the current state of the store.The trailing closure provided is given a mutable argument that represents the current state, and you can provide any mutations you want to the state. If your mutations cause the argument to differ from the current state of the test store, a test failure will be triggered.This tool is most useful in non-exhaustive test stores.Parameters:
  • updateStateToExpectedResult: A closure that asserts against the current state of the test store
public func assert(
  _ updateStateToExpectedResult: @escaping (_ state: inout State) throws -> Void
)

Dependency Overrides

withDependencies(_:operation:)
func
Overrides the store’s dependencies for a given operation.Parameters:
  • updateValuesForOperation: A closure for updating the store’s dependency values for the duration of the operation
  • operation: The operation
public func withDependencies<R>(
  _ updateValuesForOperation: (_ dependencies: inout DependencyValues) throws -> Void,
  operation: () throws -> R
) rethrows -> R
withExhaustivity(_:operation:)
func
Overrides the store’s exhaustivity for a given operation.Parameters:
  • exhaustivity: The exhaustivity
  • operation: The operation
public func withExhaustivity<R>(
  _ exhaustivity: Exhaustivity,
  operation: () throws -> R
) rethrows -> R

Usage Examples

Basic Test

@MainActor
struct CounterTests {
  @Test
  func basics() async {
    let store = TestStore(
      initialState: Counter.State(count: 0)
    ) {
      Counter()
    }
    
    await store.send(.incrementButtonTapped) {
      $0.count = 1
    }
    
    await store.send(.decrementButtonTapped) {
      $0.count = 0
    }
  }
}

Testing Effects

@Test
func testSearch() async {
  let clock = TestClock()
  
  let store = TestStore(initialState: Search.State()) {
    Search()
  } withDependencies: {
    $0.continuousClock = clock
    $0.apiClient.search = { _ in
      ["Composable Architecture"]
    }
  }
  
  // Change the query
  await store.send(.searchFieldChanged("c")) {
    $0.query = "c"
  }
  
  // Advance the clock by enough to get past the debounce
  await clock.advance(by: .seconds(0.5))
  
  // Assert that the expected response is received
  await store.receive(\.searchResponse.success) {
    $0.results = ["Composable Architecture"]
  }
}

Non-Exhaustive Testing

let store = TestStore(initialState: App.State()) {
  App()
}
store.exhaustivity = .off

await store.send(\.login.submitButtonTapped)
await store.receive(\.login.delegate.didLogin) {
  $0.selectedTab = .activity
}

Overriding Dependencies Mid-Test

store.dependencies.apiClient = .failing

await store.send(.buttonTapped) { /* ... */ }
await store.receive(\.searchResponse.failure) { /* ... */ }

store.dependencies.apiClient = .mock

await store.send(.buttonTapped) { /* ... */ }
await store.receive(\.searchResponse.success) { /* ... */ }

Exhaustive Testing

By default, TestStore requires you to exhaustively prove how your feature evolves:
  • After each action is sent you must describe precisely how the state changed
  • If an effect sends an action back into the system, you must explicitly assert that you expect to receive that action
  • All effects must complete by the time the test case has finished running

Non-Exhaustive Testing

Set exhaustivity to .off to allow:
  • Asserting on a subset of state changes
  • Sending actions even when there are unasserted received actions
  • Finishing tests with in-flight effects
Use .off(showSkippedAssertions: true) to see what assertions are being skipped without causing test failures.

Type Aliases

TestStoreOf
typealias
A convenience type alias for referring to a test store of a given reducer’s domain.
public typealias TestStoreOf<R: Reducer> = TestStore<R.State, R.Action> where R.State: Equatable
Instead of specifying two generics:
let testStore: TestStore<Feature.State, Feature.Action>
You can specify a single generic:
let testStore: TestStoreOf<Feature>

Build docs developers (and LLMs) love