機能の合成とテスト
Reducerを組み合わせ、網羅的にテストします。
「機能の合成とテスト」は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フィードバックを取得できます。ローカル設定は不要です。