Swift Academy · レッスン

機能の合成とテスト

Reducerを組み合わせ、網羅的にテストします。

レッスン 4/413 ステップ

「機能の合成とテスト」はCoddyKit上の無料Swift Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはSwift Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Swift Academyコースには全4レッスンが含まれています。

フィーチャーの合成

実際のアプリは、多くの小さなフィーチャーから構成されます。TCAでは、親フィーチャーに子フィーチャーを組み込めるため、それぞれが役割に集中しながら、より大きな全体へと組み合わせられます。そのためのツールが Scope と、子の状態およびアクションの組み込みです。

フィーチャーの合成が、The Composable Architectureという名前の由来です。

import ComposableArchitecture

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

子の状態の組み込み

親の State は、プロパティとして子の状態を保持します。親の Action 列挙型には、子のアクションを包むケースがあります。この入れ子構造は、UIの入れ子構造を反映しています。

ここでは、親がカウンターの子フィーチャーを保持しています。

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() }
}

子へのスコープ設定

Scope は、親の状態とアクションのうち、子に属する部分に対して子のリデューサーを実行します。状態のキーパスとアクションのケースキーパス、それから子のリデューサーを渡します。

これにより、子が親の更新ループに組み込まれます。

import ComposableArchitecture

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

複数のリデューサーの組み合わせ

フィーチャーの body には複数のリデューサーを記述できます。TCAは各アクションに対して、それらを記述順に実行します。これにより、親自身の Reduce と並べて1つ以上の Scope を配置できます。

ここでは、2つの子フィーチャーと親のロジックが共存しています。

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
    }
}

子ビュー用のStoreのスコープ設定

ビュー側では、子ビューに子専用のStoreが必要です。親は store.scope を使い、子の状態とアクションに対応付けてStoreを作成します。

ここでは、親ビューがスコープ設定済みのStoreをカウンタービューに渡しています。

import ComposableArchitecture
import SwiftUI

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

TestStoreの導入

TestStore を使うと、フィーチャーの振る舞いを正確に検証できます。アクションを送り、状態がどのように変化するべきかを記述します。実際の結果が異なれば、テストは失敗します。この徹底的な検証によって、見つけにくいリグレッションを検出できます。

初期状態とリデューサーを渡し、通常のStoreと同じように作成します。

import ComposableArchitecture
import XCTest

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

状態の変化を検証する

TestStore にアクションを送るときは、期待する変更を記述したクロージャを渡します。状態がそのとおりに正確に変化した場合にのみ、テストは成功します。

ここでは、インクリメントによってcountが0から1になる必要があります。

import ComposableArchitecture
import XCTest

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

網羅性

デフォルトでは、TestStore は網羅的に検証します。すべての状態変化と、副作用が返すすべてのアクションを処理しなければなりません。処理されていない変化があるとテストが失敗するため、アサーションを正確に保てます。

焦点を絞ったテストでは、store.exhaustivity = .off を設定してこの動作を緩和できます。

import ComposableArchitecture

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

副作用のアクションを受け取る

副作用がアクションを返すときは、テストでそのアクションを receive し、結果として生じる状態を検証する必要があります。これにより、非同期の処理フローが最初から最後まで機能することを確認できます。

ここでは、reloadによってレスポンスが返され、テストがそれを待って検証します。

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
    }
}

テストで依存関係を上書きする

副作用を決定的にするには、TestStore の作成時に依存関係を上書きします。偽のクライアントやテスト用のclockを渡すことで、結果を予測可能にします。

ここでは、数値クライアントを常に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 }
}

合成したフィーチャーのテスト

合成では状態とアクションが入れ子になるため、親のケースで包んだ子のアクションを送って親をテストします。同じ網羅的なアサーションを適用し、ツリー全体を検証します。

ここでは、親がスコープを通じてカウンターのインクリメントを受け取ります。

import ComposableArchitecture
import XCTest

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

クイックチェック:TestStoreの動作

デフォルトのTestStoreがアサーションに何を要求するかを思い出してください。

まとめ:合成とテスト

TCAのツールキットを一通り学びました。

  • Scope は親の状態とアクションの一部に対して子のリデューサーを実行します。また、1つの body に複数のリデューサーを組み合わせられます。
  • store.scope は、子ビューに子専用のStoreを渡します。
  • TestStore は、アクションを send したときの状態の変化を正確に検証します。
  • デフォルトで網羅的に検証し、副作用によるアクションには receive を使います。
  • withDependencies で依存関係を上書きし、決定的なテストを行えます。

これで、The Composable Architectureを使ってフィーチャーを構築、合成し、厳密にテストできるようになりました。

import ComposableArchitecture

// Recap: Scope to compose, TestStore to verify.
// Exhaustive assertions + injected fakes = confidence.
無料で開始

AI チューターと学ぶ Swift — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
122
レッスン
409

よくある質問

「機能の合成とテスト」レッスンは無料ですか?

はい。「機能の合成とテスト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Swift Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Swift Academyコースには全4レッスンが含まれています。

「機能の合成とテスト」で何を学びますか?

Reducerを組み合わせ、網羅的にテストします。 ブラウザで直接実行するハンズオンコードでSwift Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Swift Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのSwift Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「機能の合成とテスト」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このSwift Academyレッスンでコードを書いて実行できますか?

はい。すべてのSwift Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. State、Action、Reducer
  2. StoreとSwiftUIの統合
  3. Effectと依存関係
  4. 機能の合成とテスト
← Swift Academyに戻る