redux風アークテクチャのテスト
前回こちらの記事でredux風アーキテクチャの記事を書いたので、今回は各レイヤーが期待通りの挙動をしているかチェックしたいと思います
前提となる技術要件
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))
}
テストの流れ
- MockCatUseCase を使って、猫のデータ取得が成功するように設定。
- CatMiddlewareImpl を生成して fetchCat アクションを実行。
- dispatch クロージャ内で送出されるアクションをキャプチャ。
- dispatchedActions に格納されたアクションを検証。
期待するアクションの流れ
よくあるデータ取得中にローディング画面を表示し、データの取得成功後、.success
状態にするというような処理の流れになっているか確認します。
- まず「ロード中」に設定し、
- 猫データを setCat で設定し、
- 最後にロード成功状態にする。
[
.setLoadingState(state: .loading) // 「ロード中」に設定,
.setCat(cat: result) // 猫データを setCat で設定し,
.setLoadingState(state: .success) // 最後にロード成功状態にする
]
Reducer
テスト目的: Reducer(状態を更新する関数) が意図通り動作するかを検証していきます。
前提知識
Reducer: アクション (Action) と状態 (State) を受け取り、新しい状態を返す純粋関数
アクション: 状態を変更したいという「意図」
状態 (State): アプリの現在のデータ
Reducerは純粋関数にするため、以下の2つの条件を満たしています。
- 同じ引数に対して常に同じ結果を返す
- 副作用を持たない
具体的には CatAction
とCatStoreState
を受取り、新しい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 と一致しているかを検証
}
}
- テスト用の猫オブジェクト(Cat型)を生成
- cat がまだ設定されていない状態(初期状態)
-
setCat
アクションを Reducer に渡して、状態を更新 - 新しい状態に格納された 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