범위
이번 정리에서 볼 흐름
TestStore
-> Action 전송
-> Reducer 실행
-> 변경된 State 검증
지금까지는 View에서 버튼을 눌러 흐름을 봤다
TestStore는 View 없이 Reducer를 직접 검증한다
이번 예제는 처음 카운터 Feature로 돌아간다
검증할 것은 단순하다
incrementButtonTapped
-> count + 1
decrementButtonTapped
-> count - 1
대상 Feature
테스트할 Feature
@Reducer
struct CounterFeature {
@ObservableState
struct State: Equatable {
var count = 0
}
enum Action {
case decrementButtonTapped
case incrementButtonTapped
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .decrementButtonTapped:
state.count -= 1
case .incrementButtonTapped:
state.count += 1
}
return .none
}
}
}
View는 필요 없다
테스트 대상은 Reducer다
initialState
Action
expected State
이 세 가지를 비교한다
TestStore 만들기
let store = TestStore(initialState: CounterFeature.State()) {
CounterFeature()
}
TestStore에 넣는 것
initialState
-> 테스트 시작 State
CounterFeature()
-> Action을 처리할 Reducer
일반 Store와 비슷하지만 목적이 다르다
StoreOf
-> View에서 Feature 사용
TestStore
-> 테스트에서 Reducer 검증
Action 보내기
증가 버튼 Action을 테스트한다
await store.send(.incrementButtonTapped) {
$0.count = 1
}
읽는 순서
incrementButtonTapped Action을 보냄
Reducer가 State를 바꿈
바뀐 State는 count == 1이어야 함
send 뒤의 클로저는 예상 State다
{
$0.count = 1
}
Reducer가 실제로 만든 State와 이 예상 State가 다르면 테스트가 실패한다
감소 테스트
감소도 같은 방식이다
await store.send(.decrementButtonTapped) {
$0.count = -1
}
초기 State는 count == 0이다
감소 Action을 보내면 count == -1이 되어야 한다
0
-> decrementButtonTapped
-> -1
초기 State 바꾸기
테스트 시작 State를 직접 정할 수도 있다
let store = TestStore(initialState: CounterFeature.State(count: -1)) {
CounterFeature()
}
await store.send(.incrementButtonTapped) {
$0.count = 0
}
이 테스트는 -1에서 시작한다
증가 Action을 보내면 0이 되어야 한다
-1
-> incrementButtonTapped
-> 0
Swift Testing 기반 테스트 코드
import ComposableArchitecture
import Testing
@testable import UITestProj
@MainActor
struct CounterFeatureTests {
@Test
func incrementButtonTapped() async {
let store = TestStore(initialState: CounterFeature.State()) {
CounterFeature()
}
await store.send(.incrementButtonTapped) {
$0.count = 1
}
}
@Test
func decrementButtonTapped() async {
let store = TestStore(initialState: CounterFeature.State()) {
CounterFeature()
}
await store.send(.decrementButtonTapped) {
$0.count = -1
}
}
@Test
func incrementAfterDecrement() async {
let store = TestStore(initialState: CounterFeature.State(count: -1)) {
CounterFeature()
}
await store.send(.incrementButtonTapped) {
$0.count = 0
}
}
}
여기서 Swift Testing은 테스트를 감싸는 껍데기다
TestStore의 핵심은 그대로다
Action을 보냄
State 변화를 클로저에서 검증함
왜 State가 Equatable이어야 하는가
TestStore는 실제 State와 예상 State를 비교한다
그래서 State가 비교 가능해야 한다
struct State: Equatable {
var count = 0
}
Equatable이 없으면 TestStore가 State 변화가 맞는지 비교할 수 없다
공식 예제에서 State에 Equatable을 붙이는 이유가 여기서도 드러난다
TestStore가 보는 것
TestStore는 View를 보지 않는다
다음만 본다
Action을 보냈는가
Reducer가 State를 어떻게 바꿨는가
Effect가 추가 Action을 보냈는가
이번 카운터 예제는 Effect가 없다
그래서 send만 검증한다
Effect가 있는 경우에는 receive로 Effect가 되돌려 보낸 Action까지 검증한다
await store.receive(.response) {
$0.message = "완료"
}
이번 글에서는 Effect 검증까지 깊게 보지 않는다
현재 기준
Reducer는 규칙이다
TestStore는 그 규칙을 실행해서 결과를 확인한다
카운터 예제 기준
State
-> count
Action
-> incrementButtonTapped
-> decrementButtonTapped
검증
-> Action 이후 count가 예상대로 바뀌었는가
정리
현재 이해한 TestStore
TestStore = View 없이 Reducer를 실행하는 테스트용 Store
send = Action을 보내고 State 변화를 검증
receive = Effect가 되돌려 보낸 Action을 검증
Equatable State = 실제 State와 예상 State를 비교하기 위한 조건
TCA에서 테스트는 View 이벤트를 흉내 내는 방식이 아니다
Reducer에 Action을 보내고 State 변화가 정확한지 확인하는 방식이다