Xcode UnitTest ① - RxTest!! 테스트 스케줄러

4 분 소요


목차

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을 구독할 수 있음

테스트 스케줄러 상에서 생성한 이벤트들을 발행하고 하면 전부 추적 가능하고, 테스트 가능함!!


전제

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의 로직이

  1. email, password1, password2: 사용자의 입력
  2. email은 알맞은 형식이어야 함(xxxx@xx.xx)
  3. password1은 알맞은 형식이어야 함(대소문자 포함 등)
  4. 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 email 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가 됨


굿


태그: ,

카테고리:

업데이트:

댓글남기기