보통의 유닛테스트라면
지난 글에서처럼 각 메서드들의 Input에 따른 Output값을
XCTest의 XCTAssertion을 통해서 테스트코드를 작성하면 됩니다.
XCTest와 Nimble
기능을 구현하는 것도 중요하지만, '구현된 기능이 의도한 대로 당연하게 동작하는가?'를 테스트해보는 테스트 코드의 작성도 중요합니다. 배포를 하기 전에 사전에 문제를 발견할 수 있는 좋은
swifty-cody.tistory.com
하지만 RxSwift로 작성한 코드의 유닛테스트는 어떻게 할까요?
RxSwift에서 사용하는 Observable은 '값이 아니라 시퀀스'이기 때문에 XCTest로 바로 테스트를 작성하기 어려울 수 있는데,
이를 위해서 RxTest와 RxBlocking이 준비되어 있습니다.
(우리 Rx는 계획이 다 있구나?!)
RxTest는 시간, 타이밍의 개념이 필요한 테스트의 경우 주로 사용되고,
RxBlocking은 그렇지 않은 테스트의 경우 사용됩니다.
이번 포스팅에서는 그 중에 RxTest를 정리합니다.
RxTest
RxTest에서 제공하는 TestScheduler를 이용해서 XCTestCase 내에서
실제 사용했을 때처럼 시간의 흐름에 따라 원하는 이벤트의 Observable들을 등록해놓고,
start()를 호출하면 등록해놓은 흐름에 따라 데이터가 방출되는 것을 구현할 수 있습니다.
예제에서 사용된 코드는 글 아래에 레포를 공유되어 있습니다.
사용자의 위치를 받거나, MapView의 변경된 Center위치를 받으면
Kakao로컬API를 통해 500m 반경 편의점 위치를 받아와서 MapView에 표시해주는 예제입니다.
다음은 TestScheduler를 0초로 초기화하여 생성하고,
시작하자마자(0초에) 통신으로 데이터를 받은 것처럼 더미데이터를 세팅하는 예제입니다.
func testSetMapCenter() {
let scheduler = TestScheduler(initialClock: 0)
// 더미데이터의 이벤트
let dummyDataEvent = scheduler.createHotObservable([
.next(0, cvsList) // 0초에 cvsList(더미데이터)를 통신으로 받은 것처럼 설정
])
}
'더미데이터'
실제의 코드는 네트워크 통신을 이용할 수 있지만, 네트워크 환경은 우리가 제어할 수 있는 환경이 아니기 때문에
테스트 목적에 적합하지 않습니다.
우리의 유닛테스트 목적은 네트워킹이 잘 되는지를 테스트하는 것이 아니라,
Model, ViewModel이 잘 동작하는지를 테스트하기 위함이기 때문이죠.
그래서 네트워크를 통한 데이터가 필요한 경우엔 미리 더미데이터를 setUp()에서 준비해놓습니다.
'HotObservable'
ReactiveX에서는 HotObservable과 ColdObservable이 있습니다.
공식 설명으로는 이렇습니다.
When does an Observable begin emitting its sequence of items? It depends on the Observable. A “hot” Observable may begin emitting items as soon as it is created, and so any observer who later subscribes to that Observable may start observing the sequence somewhere in the middle. A “cold” Observable, on the other hand, waits until an observer subscribes to it before it begins to emit items, and so such an observer is guaranteed to see the whole sequence from the beginning.
HotObservable은 생성 즉시 값을 방출하기 시작해서 나중에서 subscribe한 Observable은 시퀀스 중간 어딘가에서 값을 받을 수가 있게 됩니다.
Subject, Relay 같은 성격의 Observable의 경우 HotObservable에 속한다고 볼 수 있습니다.
UIEvent, Location이벤트 등을 받는 PublishRelay가 이런 종류라고 볼 수 있죠.
하나의 HotObservable 이벤트를 여러 Observer가 받아서 각 Observer의 입맛에 맞게 처리가 가능합니다.
ColdObservable은 Observer가 Subscribe하기 전까지 방출을 기다리고 있기 때문에, Observer는 전체 시퀀스를 받을 수 있게 됩니다.
Observable의 Create 생성 operator들, create, of , just, from 및 Traits(Single, Maybe, Completable) 같은 성격의 Observable들이 ColdObservable에 속합니다.
서버로부터 Data를 fetch해오는 작업이 이런 종류에 속합니다.
이어서 1초가 되었을 때 사용자가 0번째 셀을 눌렀다고 가정하면
해당 IndexPath.row 즉 0을 PublishRelay로 받기 때문에 다시 HotObservable을 생성하여 아래처럼 작성해줄 수 있습니다.
MVVM, MVI 패턴 등으로 View와 로직(ViewModel, Intent), Model을 분리시켜주면 이런 테스트코드 작성이 편해지는 이점이 있는거죠.
func testSetMapCenter() {
// (위 코드에 이어서)
let documentData = PublishSubject<[KLDocument]>()
dummyDataEvent
.subscribe(documentData)
.disposed(by: disposeBag)
// DetailList 아이템(셀) 탭 이벤트
let itemSelectedEvent = scheduler.createHotObservable([
.next(1, 0) // 1초에 0번째 셀이 선택된 것처럼 설정
])
// 선택된 셀 row에 따른 Data 세팅
let itemSelected = PublishSubject<Int>()
itemSelectedEvent
.subscribe(itemSelected)
.disposed(by: disposeBag)
let selectedItemMapPoint = itemSelected
.withLatestFrom(documentData) { $1[0] }
.map(model.documentToCoordinate)
위 코드에 이어서 나머지 시간대의 이벤트들도 마저 작성합니다.
func testSetMapCenter() {
// (위 코드에 이어서)
// 최초 현재 위치 이벤트
let initialCoordinate = CLLocationCoordinate2D(latitude: 37.394225, longitude: 127.110341)
let currentLocationEvent = scheduler.createHotObservable([
.next(0, initialCoordinate)
])
let initialCurrentLocation = PublishSubject<CLLocationCoordinate2D>()
currentLocationEvent
.subscribe(initialCurrentLocation)
.disposed(by: disposeBag)
// 현재 위치 버튼 탭 이벤트 (2번 탭 했다고 가정)
let currentLocationButtonTapEvent = scheduler.createHotObservable([
.next(2, Void()),
.next(3, Void())
])
let currentLocationButtonTapped = PublishSubject<Void>()
currentLocationButtonTapEvent
.subscribe(currentLocationButtonTapped)
.disposed(by: disposeBag)
let moveToCurrentLocation = currentLocationButtonTapped
.withLatestFrom(initialCurrentLocation)
// merge
let currentMapCenter = Observable
.merge(
selectedItemMapPoint,
initialCurrentLocation.take(1),
moveToCurrentLocation
)
}
위처럼 가상의 시간대별 스케쥴을 작성해주고 나면, 이제 이 값들이 의도한대로 잘 나오는지 검증을 해줄 차례입니다.
TestScheduler는 임의의 Observer를 생성시켜 subscribe여부와 상관없이 Observable의 값들을 검증할 수 있습니다.
TestScheduler를 start()해주면 등록한 HotObservable, ColdObservable들이 가상의 스케쥴에 따라 동작을 합니다.
그 후에 XCTestAssert, Nimble 등을 통해서 결과의 비교를 해주면 됩니다.
여기서는 Nimble을 통해 각 시간대별 이벤트 결과값들을 한꺼번에 비교해주었습니다.
func testSetMapCenter() {
// (위 코드에 이어서)
// 위에서 등록한 Observable들(의 coordinate.latitude값)을 검증을 위한 Observer 생성
let currentMapCenterObserver = scheduler.createObserver(Double.self)
currentMapCenter
.map { $0.latitude }
.subscribe(currentMapCenterObserver)
.disposed(by: disposeBag)
scheduler.start() // TestScheduler의 동작
let secondCoordinate = model.documentToCoordinate(doc[0])
// equal은 Equatable 프로토콜을 따라야 해서 coordinate를 비교하기 보다, coordinate의 값을 비교
expect(currentMapCenterObserver.events).to(
equal([
.next(0, initialCoordinate.latitude),
.next(1, secondCoordinate.latitude),
.next(2, initialCoordinate.latitude),
.next(3, initialCoordinate.latitude)
])
)
}
전체 소스는 아래에 공유합니다.
GitHub - redxoul/SearchCVS: Kakao RestAPI를 이용한 RxCocoa 예제
Kakao RestAPI를 이용한 RxCocoa 예제. Contribute to redxoul/SearchCVS development by creating an account on GitHub.
github.com