😬
[TCA]dismissのUnitTest
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にしているもの。
Discussion