Xcode UnitTest ① - RxTest!! 테스트 스케줄러
목차
1: Xcode UnitTest ① - RxTest!! 테스트 스케줄러
2: Xcode UnitTest ② - RxSwift + Moya!! 네트워크도 목업으로 테스트
3: Xcode UnitTest ③ - RxSwift + Moya 네트워크 딜레이 테스팅!!
4: Xcode UnitTest ④ - 병렬 처리!! 퍼포먼스 테스팅(measure)
서론의 서론
우리 프로젝트가 출시도 하고 마무리도 했었다
개발하면서 정말 많이 배웠음… 진짜 주변에 열심히 잘 하는 사람들이 있는 게 좋다
아무튼 그 후
놀고 하고 싶은 거 하고 포켓몬도 하고 건담도 보고 만들고 개발은 취미로 가끔 하고 그렇게 느긋하게 살았다
그림은 매일 그리며 연습하지 않아도, 시간만 지나도 실력이 늘게 된다. 관찰하는 법을 배웠기 때문에, 평소에 관찰한 지식으로 그릴 수 있기 때문
코딩도 그렇고… 대부분의 일들이 그렇지 않을까요?? 한 번 발 들여 놓으면 평상시에도 무의식 중에라도 사고 방식이 약간 그 쪽으로? 생각하게 되고 하니까 그것도 무의식 중의 경험치입니다
결론: 좀 놀아도 괜찮은 듯
ㅋㅋ저까지는 변명이었고 다시 돌아와서
같이 iOS 개발하던 선배는 취직해서 이제 이 프젝의 iOS 레포는 내 세상이 되었다
혼자 정체되지 않게 리팩토링&테스트 코드 작성 시작!!
그리고 노션에 적어 놨던 내용들도 블로그에 좀 올려야 할 듯…
서론(테스트 코드의 장점)
그리고 테스트 코드에 좀 중독 되었다.
어떻게 하면 테스트 가능할까에 매몰되어서 기존 코드 다 뜯어 고치고 깊은 수렁에 빠질 뻔도 하였으나
나름 정도를 찾은 것 같음
생각해 보면 테스트 코드 짜는 게 참 재밌는 일임
처음 코딩이 너무 좋다 생각하게 된 계기가 내가 생각한 대로 짜니까 코드가 돌아가는 게 재밌어서였는데
테스트 코드는 진짜 생각한대로 돌아갈까? 이건 어떰? 이거도 방어 가능? 하면서 계속 개선하게 되고
직접하는 UI 테스팅으로는 좀 곤란한 부분도 rx로 네트워크 딜레이까지 고려하면서 예측한 시간에 제대로 동작하는가도 테스팅이 가능하니(내가 이 코드를 테스트 가능하게 만들었다, 나는 시간을 지배할 수 있다!)
테스트 케이스가 있으면 리팩토링도 마음 놓고 할 수 있음.
리팩토링 하다가 테스트 함 돌려 보고 갑자기 🆇 뜨면 어 씨 이거 왜 이래 하고 바로 고치는 게 가능하다
이제 본론으로
RxSwift In Testing
테스팅에서 rx가 동작하게 하기 위해서는, 스케줄러를 생성 후 어떤 시점 t에 특정 이벤트를 방출하도록 지정을 해주어야 한다
?
- 그냥 릴레이에
accept()
로 이벤트 넣어 주고isValidForm
검사해 주면 안 됨?- ⇒ 안 됨
- 이벤트가 언제 발생하는 지 전혀 알 수가 없음!!
- ⇒ 안 됨
!
그래서 테스트 스케줄러를 사용함
TestScheduler
- Test에서 clock에 따라 이벤트 처리를 할 수 있는 가상의 스케줄러
TestableObservable<T>
- Test 가능한 Observable. 이벤트들을 저장해 둠
TestableObserver<T>
- Test 가능한 Observer.
TestableObservable
을 구독할 수 있음
- Test 가능한 Observer.
테스트 스케줄러 상에서 생성한 이벤트들을 발행하고 하면 전부 추적 가능하고, 테스트 가능함!!
전제
ViewModel이 Input/Output 패턴을 따르는 상태
class RegisterViewModel: ViewModel {
struct Input {
let emailTextField: Observable<String?>
let password1TextField: Observable<String?>
let password2TextField: Observable<String?>
}
struct Output {
let isValidForm: Observable<Bool>
}
func transform(input: Input) -> Output { ... }
}
간단 간단한 예로…
ViewModel의 로직이
- email, password1, password2: 사용자의 입력
- email은 알맞은 형식이어야 함(xxxx@xx.xx)
- password1은 알맞은 형식이어야 함(대소문자 포함 등)
- password2는 password1과 같아야 함
위의 조건들을 만족했을 때, Output인 isValidForm
이 true가 된다고 하자
- +
isValidForm
이 true가 되었다가도, 위의 조건들이 바뀌면 false가 되어야 함
주의!!!
만약 로직 중에 observe(on:)
이나 subscribe(on:)
으로 다른 스레드에서 관찰하는 부분이 있다면 테스트 스케줄러에서 추적 불가능하므로 테스팅이 안 됨!!!
테스트 코드
import XCTest
import RxSwift
import RxTest
@testable import TARGET_PROJECT
Tests 폴더에서 Unit Test Case Class 파일을 생성 후, RxSwift와 RxTest를 추가로 import 합니다.
테스트할 타겟 프로젝트도 import
0.
class RegisterTests: XCTestCase {
var registerViewModel: RegisterViewModel!
var scheduler: TestScheduler!
var disposeBag: DisposeBag!
override func setUpWithError() throws {
registerViewModel = RegisterViewModel()
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
}
}
테스트 시작 전 세팅을 위해 setUpWithError()
에서 초기화를 해줌시다
setUpWithError()
,tearDownWithError()
- 기존
setUp()
,tearDown()
에서 에러를 던질 수 있도록 바뀌었다고 하네여
- 기존
테스트하고 싶은 ViewModel 인스턴스 생성, 테스팅에서 rx를 동작하게 하기 위해 가상의 스케줄러와 disposeBag도 생성
1.
func testValidating() throws {
let emailEvents: TestableObservable<String?> = scheduler.createHotObservable([
.next(0, ""),
.next(1, "email@email"),
.next(2, "email@email.com"),
.completed(10)])
let password1Events: TestableObservable<String?> = scheduler.createHotObservable([
.next(0, ""),
.next(3, "qwer"),
.next(4, "qwertY"),
.next(5, "qwertY1!"),
.next(8, "qwe1!"),
.completed(10)])
let password2Events: TestableObservable<String?> = scheduler.createHotObservable([
.next(0, ""),
.next(6, "qwertY1"),
.next(7, "qwertY1!"),
.completed(10)])
// ...
}
이제 이벤트 제작!! 스케줄러의 클럭에 따라 입력 해줄 수 있음
2.
func testValidating() throws {
// ...
let isValidForm = scheduler.createObserver(Bool.self)
// ...
}
Output을 관찰하기 위해 Observer를 생성
3.
func testValidating() throws {
// ...
let input = RegisterViewModel.Input(emailTextField: emailEvents.asObservable(),
password1TextField: password1Events.asObservable(),
password2TextField: password2Events.asObservable())
let output = registerViewModel.transform(input: input)
// ...
}
이제 1에서 생성한 이벤트들을 Observable로 넣어주고, 뷰모델에서 아웃풋을 받아와 본다!!
4.
func testValidating() throws {
// ...
output.isValidForm.bind(to: isValidForm).disposed(by: disposeBag)
// ...
}
Output을 2에서 생성한 Observer들에 바인딩해준다.
이제 사용자의 입력 이벤트들에 따른 출력 메시지들을 관찰할 수 있다
사용
5.
func testValidating() throws {
// ...
scheduler.start()
// ...
}
스케줄러 시작
6.
event time | password1 | password2 | isVlidForm | |
---|---|---|---|---|
0 | “” | “” | “” | F |
1 | “email@email” | F | ||
2 | “email@email.com” | F | ||
3 | “qwer” | F | ||
4 | “qwertY” | F | ||
5 | “qwertY1! | F | ||
6 | “qwertY1” | F | ||
7 | “qwertY1!” | T | ||
8 | “qwe1!” | F |
예상되는 이벤트는 위와 같다.
func testValidating() throws {
// ...
XCTAssertEqual(isValidForm.events, [.next(0, false),
.next(7, true),
.next(8, false)])
}
최종 bool 값 isValidForm
을 확인해보자(.distinctUntilChanged()
사용하여 값이 같은 것들은 스킵된 상태)!!
클럭 7에서 전부 완벽하게 입력 되었으므로 true
가 방출되고, 클럭 8에서 password1의 값이 바뀌어 false
가 됨
굿
댓글남기기