0Pricing
Swift Academy · Lesson

Composing and Testing Features

Combine reducers and test exhaustively.

Composing and Testing Features is a free Swift Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Composing Features

Real apps are built from many small features. TCA lets a parent feature embed child features so each stays focused, then combines them into a larger whole. The tools for this are Scope and embedding child state and actions.

Composition is what gives The Composable Architecture its name.

import ComposableArchitecture

// A parent feature embeds child State and Action,
// then uses Scope to run the child's reducer.

Embedding Child State

The parent's State holds the child's state as a property. The parent's Action enum has a case wrapping the child's actions. This nesting mirrors how the UI nests.

Here a parent holds a counter child.

import ComposableArchitecture

@Reducer
struct AppFeature {
    @ObservableState
    struct State: Equatable {
        var counter = CounterFeature.State()
    }
    enum Action {
        case counter(CounterFeature.Action)
    }
    var body: some ReducerOf<Self> { EmptyReducer() }
}

Scoping to the Child

Scope runs the child reducer on the slice of parent state and action that belong to it. You give it a state key path and an action case path, then the child reducer.

This wires the child into the parent's update loop.

import ComposableArchitecture

var body: some ReducerOf<Self> {
    Scope(state: \.counter, action: \.counter) {
        CounterFeature()
    }
    Reduce { state, action in
        // parent-specific logic here
        return .none
    }
}

Combining Multiple Reducers

A feature's body can list several reducers; TCA runs them in order for each action. This lets you place one or more Scopes alongside the parent's own Reduce.

Here two children and parent logic coexist.

import ComposableArchitecture

var body: some ReducerOf<Self> {
    Scope(state: \.profile, action: \.profile) { ProfileFeature() }
    Scope(state: \.settings, action: \.settings) { SettingsFeature() }
    Reduce { state, action in
        return .none
    }
}

Scoping the Store for Child Views

On the view side, a child view needs a store specialized to the child. The parent derives it with store.scope, mapping into the child's state and action.

Here the parent view passes a scoped store to the counter view.

import ComposableArchitecture
import SwiftUI

struct AppView: View {
    let store: StoreOf<AppFeature>
    var body: some View {
        CounterView(
            store: store.scope(state: \.counter, action: \.counter)
        )
    }
}

Introducing TestStore

The TestStore lets you assert exactly how a feature behaves. You send actions and describe how state must change; if reality differs, the test fails. This exhaustive checking catches subtle regressions.

You create it like a normal store, with initial state and the reducer.

import ComposableArchitecture
import XCTest

func testIncrement() async {
    let store = await TestStore(initialState: CounterFeature.State()) {
        CounterFeature()
    }
}

Asserting State Changes

When you send an action to a TestStore, you pass a closure describing the expected mutation. The test passes only if the state changes exactly that way.

Here incrementing must raise count from 0 to 1.

import ComposableArchitecture
import XCTest

func testIncrement() async {
    let store = await TestStore(initialState: CounterFeature.State()) {
        CounterFeature()
    }
    await store.send(.incrementTapped) {
        $0.count = 1
    }
}

Exhaustivity

By default a TestStore is exhaustive: you must account for every state change and every action an effect feeds back. Unhandled changes fail the test, which keeps your assertions honest.

You can relax this with store.exhaustivity = .off for focused tests.

import ComposableArchitecture

// Exhaustive (default): every change must be asserted.
// Relax when you only care about part of the behavior:
store.exhaustivity = .off

Receiving Effect Actions

When an effect sends an action back, the test must receive it and assert the resulting state. This proves the asynchronous flow works end to end.

Here a reload triggers a response the test awaits and checks.

import ComposableArchitecture
import XCTest

func testReload() async {
    let store = await TestStore(initialState: NumberFeature.State()) {
        NumberFeature()
    }
    await store.send(.reload)
    await store.receive(\.response) {
        $0.value = 42
    }
}

Overriding Dependencies in Tests

To make effects deterministic, override dependencies when constructing the TestStore. Provide a fake client or a test clock so results are predictable.

Here the number client is replaced with one that always returns 42.

import ComposableArchitecture
import XCTest

func testReload() async {
    let store = await TestStore(initialState: NumberFeature.State()) {
        NumberFeature()
    } withDependencies: {
        $0.numberClient.fetch = { 42 }
    }
    await store.send(.reload)
    await store.receive(\.response) { $0.value = 42 }
}

Testing Composed Features

Because composition nests state and actions, you test a parent by sending child actions wrapped in the parent case. The same exhaustive assertions apply, verifying the whole tree.

Here a parent receives a counter increment through its scope.

import ComposableArchitecture
import XCTest

func testChildInParent() async {
    let store = await TestStore(initialState: AppFeature.State()) {
        AppFeature()
    }
    await store.send(.counter(.incrementTapped)) {
        $0.counter.count = 1
    }
}

Quick Check: TestStore Behavior

Recall what a default TestStore requires of your assertions.

Recap: Composing and Testing

You completed the TCA toolkit:

  • Scope runs a child reducer on a slice of parent state and action, and several reducers can be combined in one body.
  • store.scope hands a child view its own specialized store.
  • TestStore asserts exact state changes when you send actions.
  • It is exhaustive by default and uses receive for effect-driven actions.
  • Override dependencies with withDependencies for deterministic tests.

You can now build, compose, and rigorously test features with The Composable Architecture.

import ComposableArchitecture

// Recap: Scope to compose, TestStore to verify.
// Exhaustive assertions + injected fakes = confidence.

Frequently asked questions

Is the “Composing and Testing Features” lesson free?

Yes — the full text of “Composing and Testing Features” is free to read here on the web, and the Swift Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Swift Academy course, upgrade to CoddyKit PRO.

What will I learn in “Composing and Testing Features”?

Combine reducers and test exhaustively. You practise Swift Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Swift Academy?

No prior experience is required. Swift Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Composing and Testing Features” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Swift Academy lesson?

Yes. Every Swift Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. State, Action, and Reducer
  2. The Store and SwiftUI Integration
  3. Effects and Dependencies
  4. Composing and Testing Features
← Back to Swift Academy