Zenn
📝

redux風アークテクチャのテスト

2025/03/24に公開

前回こちらの記事でredux風アーキテクチャの記事を書いたので、今回は各レイヤーが期待通りの挙動をしているかチェックしたいと思います
https://zenn.dev/ohayoukenchan/articles/b3f7bc52e289a9

前提となる技術要件

iOS16+ なのでswift-testingライブラリを使います

  • iOS16+
  • swift-testingの TestingFramework
  • この記事は 前回の続きとなりますのであらかじめご確認いただけますとスムーズかと思います

Test詳細

Middleware

テスト目的:Actionを処理したときに、期待されるActionが dispatch されるか確認したい。

@Suite("CatMiddlewareTests")
struct CatMiddlewareTests {
    @Test("fetchCatが成功したとき_setCatを実行する")
    @MainActor
    func fetchCat() async throws {
        var dispatchedActions: [CatAction] = []

        ///  1. MockCatUseCase を使って、猫のデータ取得が成功するように設定。
        let mockUseCase = MockCatUseCase()

        let result = createMockCat()

        mockUseCase.mockCat = result

        // Arrange: CatMiddlewareImplを生成
        let middleware = CatMiddlewareImpl(useCase: mockUseCase)

        // Act: handleActionを実行
        /// 2. CatMiddlewareImpl を生成して fetchCat アクションを実行。
        await middleware.handleAction(
            action: .fetchCat,
            state: CatStoreState(
                cat: nil
            ),
            dispatch: { action in
                /// 3. dispatch クロージャ内で送出されるアクションをキャプチャ。
                dispatchedActions.append(action)
            }
        )

        // Assert: dispatchされたAction
        /// 4. dispatchedActions に格納されたアクションを検証。
        #expect(dispatchedActions.count == 3)

        #expect(dispatchedActions[0] == .setLoadingState(state: .loading))
        #expect(dispatchedActions[1] == .setCat(cat: result))
        #expect(dispatchedActions[2] == .setLoadingState(state: .success))
    }

テストの流れ

  1. MockCatUseCase を使って、猫のデータ取得が成功するように設定。
  2. CatMiddlewareImpl を生成して fetchCat アクションを実行。
  3. dispatch クロージャ内で送出されるアクションをキャプチャ。
  4. dispatchedActions に格納されたアクションを検証。

期待するアクションの流れ

よくあるデータ取得中にローディング画面を表示し、データの取得成功後、.success状態にするというような処理の流れになっているか確認します。

  1. まず「ロード中」に設定し、
  2. 猫データを setCat で設定し、
  3. 最後にロード成功状態にする。
[
    .setLoadingState(state: .loading) // 「ロード中」に設定,
    .setCat(cat: result) // 猫データを setCat で設定し,
    .setLoadingState(state: .success) // 最後にロード成功状態にする
]

Reducer

テスト目的: Reducer(状態を更新する関数) が意図通り動作するかを検証していきます。

前提知識

Reducer: アクション (Action) と状態 (State) を受け取り、新しい状態を返す純粋関数
アクション: 状態を変更したいという「意図」
状態 (State): アプリの現在のデータ

Reducerは純粋関数にするため、以下の2つの条件を満たしています。

  • 同じ引数に対して常に同じ結果を返す
  • 副作用を持たない

具体的には CatActionCatStoreStateを受取り、新しいCatStoreStateを返します

func catReducer(action: CatAction, state: CatStoreState) -> CatStoreState {
    var newState = state
    switch action {
    case .fetchCat:
        break // Middlewareに任せる
    case .setCat(let cat):
        newState.cat = cat
    case .setLoadingState(let loadingState):
        newState.loadingState = loadingState
    }
    return newState
}

テストの流れ

testコードに関しては短いのでそのまま掲載します。処理の流れは以下のとおりです。

@Suite("CatReducerTests")
struct CatReducerTests {
    @Test("setCatが成功する")
    func setCat() {
        let result = createMockCat() /// 1.テスト用の猫オブジェクト(Cat型)を生成

        let initialState = CatStoreState(cat: nil) /// 2. cat がまだ設定されていない状態(初期状態)
        let newState = catReducer(action: .setCat(cat: result), state: initialState) /// 3.  `setCat`アクションを Reducer に渡して、状態を更新

        #expect(newState.cat == result) /// 新しい状態に格納された cat が、期待値 result と一致しているかを検証
    }
}

  1. テスト用の猫オブジェクト(Cat型)を生成
  2. cat がまだ設定されていない状態(初期状態)
  3. setCatアクションを Reducer に渡して、状態を更新
  4. 新しい状態に格納された cat が、期待値 result と一致しているかを検証

このように、新しい状態に格納されたcatが、期待値resultと一致しているかを検証でき、Reducerが正しく動作していることが確認できます

Store

apply

store.apply(state:) を呼ぶと、ストアの内部状態が正しく更新されるかどうかを確認されるかのテストです

@Test("applyでstateが反映される")
func testApply() {
    let store = CatStore.shared
    let mockCat = createMockCat()

    let state = CatStoreState(cat: mockCat, loadingState: .success)

    store.apply(state: state)

    #expect(store.cat == mockCat)
    #expect(store.loadingState == .success)
}

createCatState

現在のストアの状態を正しく表す CatStoreState を返すことを確認します

@Test("createCatStateが現在の状態を返す")
func testCreateCatState() {
    let store = CatStore.shared
    let mockCat = createMockCat()

    store.apply(state: .init(cat: mockCat, loadingState: .loading))

    let state = store.createCatState()

    #expect(state.cat == mockCat)
    #expect(state.loadingState == .loading)
}

dispatch

.fetchCat アクションが成功したときに、ストアの状態が正しく更新されることを確認

@Test("dispatchでCat取得成功のフローが動く")
@MainActor
func testDispatchSuccess() async throws {
    let useCase = MockCatUseCase()
    let mockCat = createMockCat()
    useCase.mockCat = mockCat

    let middleware = MockCatMiddleware(useCase: useCase)
    let store = CatStore(middleware: middleware)

    await store.dispatch(.fetchCat)

    #expect(store.cat == mockCat)
    #expect(store.loadingState == .success)
}

ViewState

ViewStateはstoreが更新されたときに更新した値が反映されているか、
ActionDispatcherが適切に発火しているかなどの確認をします

より具体的なサンプルをこちらにありますので合わせてご確認ください

まとめ

Redux(のような)アーキテクチャは明確に役割が分離されているため、それぞれのレイヤーに対して個別に**責務を確認(大事)**することで、どこになにを書けばいいか迷わなくて良さそうです

レイヤー 役割 テスト観点
Middleware 非同期処理・副作用の管理 アクションのフロー
Reducer 純粋関数による状態変換 入出力の一致
Store 状態の集中管理 状態の反映と取得
ViewState ViewとStoreの橋渡し 状態バインディング・アクション伝播

Discussion

ログインするとコメントできます