😬

[TCA]dismissのUnitTest

2024/10/31に公開

TL, DR

TCAのdismiss DependencyをUnitTestする方法を記事にする。
他のDependencyのUnitTestでも使えそう。

環境

Xcode: 16
TCA: 1.15.2

Sample App

以下の仕様のアプリを考える

  • Parent Screenには、"ShowChild" ボタンを表示し、タップすると、ChildViewに遷移する(Push)。
  • Child Screenには、 "Dismiss" ボタンを表示し、タップすると、ParentViewに遷移する(Pop)。

アプリの動画

Parent Screenの実装

// MARK: Parent Screen
@Reducer
struct ParentReducer {
    @ObservableState
    struct State: Equatable {
        @Presents var child: ChildReducer.State?
    }

    enum Action: Sendable {
        case child(PresentationAction<ChildReducer.Action>)
        case showChild
    }

    var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .child:
                return .none
            case .showChild:
                state.child = ChildReducer.State()
                return .none
            }
        }.ifLet(\.$child, action: \.child) {
            ChildReducer()
        }
    }
}

struct ParentView: View {
    @Bindable var store: StoreOf<ParentReducer>
    var body: some View {
        Button("ShowChild") {
            store.send(.showChild)
        }
        .navigationDestination(item: $store.scope(state: \.child, action: \.child)) { store in
            ChildView(store: store)
        }
        .navigationTitle("Parent")
    }
}

Child Screenの実装

// MARK: Child Screen
@Reducer
struct ChildReducer {
    @ObservableState
    struct State: Equatable {}

    enum Action: Sendable {
        case tapButton
    }

    @Dependency(\.dismiss) var dismiss

    var body: some Reducer<State, Action> {
        Reduce { _, action in
            switch action {
            case .tapButton:
                return .run { _ in
                    await dismiss()
                }
            }
        }
    }
}

struct ChildView: View {
    @Bindable var store: StoreOf<ChildReducer>

    var body: some View {
        Button("Dismiss") {
            store.send(.tapButton)
        }
        .navigationTitle("Child")
    }
}

UnitTest

ParentReducerのテスト

class ParentTests: XCTestCase {
    @MainActor
    func testShowChild() async {
        let store = TestStore(initialState: ParentReducer.State()) {
            ParentReducer()
        }

        await store.send(.showChild) {
            $0.child = ChildReducer.State()
        }
    }
}

ChildReducerのテスト

class ChildTests: XCTestCase {
    @MainActor
    func testDismissChild() async {
        let isDismissInvoked: LockIsolated<[Bool]> = .init([])
        let store = TestStore(initialState: ChildReducer.State()) {
            ChildReducer()
        } withDependencies: {
            $0.dismiss = DismissEffect {
                isDismissInvoked.withValue {
                    $0.append(true)
                }
            }
        }

        await store.send(.tapButton)
        XCTAssertEqual(isDismissInvoked.value, [true])
    }
}

ポイント

  • dismissが呼ばれたかどうかを、isDismissInvokedで記録する。listを使用していることで、1回だけ呼ばれていることをテストできる。
  • LockIsolatedは、TCAが提供しているclassで、muテーブルな値をlockで、concurrenct-safeにしているもの。

References

Discussion