How to run tests in RxSwift 및 적용해보기

2023. 11. 28. 15:39Swift/RxSwift

반응형

How to run tests in RxSwift

  • 아래 내용은 밑의 참고 링크를 일부 번역하고 약간 변형을 주어 공부한 내용입니다.
  • 번역에 오류가 있거나 이해한 것이 틀릴 수 있습니다.

참고 - How to run tests in RxSwift

  • RxTestRxBlocking의 경우 RxSwift 레포지토리 중 일부이지만 분리 되어있기 때문에 따로 불러 올 필요가 있습니다.

  • SPM을 사용했을 경우 RxSwift와 RxCocoa만을 import했다면 추가적으로 불러와서 사용해야합니다.

  • RxTest 의 경우 Rx코드를 테스팅 하는데 매우 유용한 추가기능을 제공합니다. 특히, TestScheduler의 경우 가상의 시간을 설정하여 특정 시간마다 이벤트를 발생 시킬 수 있습니다.

  • RxBlocking 의 경우 일반적인 Observable시퀀스를 변환하여 관찰 가능하도록 만듭니다.

  • 이는, 관찰 가능한 시퀀스로 변환되거나 일정 시간을 초과할 때까지 실행 중인 스레드를 차단하는데, 이로 인해 비동기 작업을 훨씬 쉽게 테스트할 수 있습니다.

RxTest

  • Rx를 테스트 하기 전 RxTest에 대해 알아야할 것이 있습니다.
  • RxTest는 테스트 목적으로 두 가지 Observable이 존재합니다.
    1. HotObservables
      • 구독 여부에 상관없이 특정 시간에 정해진 이벤트를 발생시킨다.
    2. ColdObservables
      • 일반적으로 사용한 Observables와 같이 새로운 구독이 발생한다면 그 때 새로운 요소를 구독자에게 요청한다.

RxBlocking

  • XCTest*의 expectations와 유사하게 *RxBlocking 역시 비동기 코드를 테스트 가능한 연산자 중 하나이다.

Testing with RxBlocking

  • of를 사용하여 10, 20, 30 요소를 가지는 Observable배열을 만들고 이것을 테스트 할 예정이다.
let observableToTest = Observable.of(10, 20, 30)
let result = observableToTest.toBlocking()
  • toBlocking()메서드를 통해 observableToTest 시퀀스를 관찰 가능하도록 만들어 줄 수 있다.

  • toBlocking메서드는 블록킹 된 Observable을 반환하며 직선적인 배열로 확인할 수 있다.
  • BlockingObservable객체의 first메서드를 활용하여 첫번째 요소를 확인할 수 있으며, 일반적인 배열과는 다르게 에러를 throw 할 수 있어 do-catch문을 통하여 확인하여야 한다.
let observableToTest = Observable.of(10, 20, 30)

do {
    let result = try observableToTest.toBlocking().first()
    XCTAssertEqual(result, 10)
} catch {
    XCTFail(error.localizedDescription)
}
  • first메서드가 에러를 반환한다면 실패한 테스트이므로 catch문에는 XCTFail을 넣어주었다.

  • 만약 성공한다면 XCTAssertEqual을 사용하여 우리가 만든 Observable의 첫번째 요소가 10인지를 확인한다.

  • 다만, 테스트코드란 점에서 위와 같은 코드는 생산성이 저해되므로 다음과 같은 코드가 상태를 확인하고 작성하기 더 용이하다.

XCTAssertEqual(10, try! observableToTest.toBlocking().first())
  • 다만 위와 같은 테스트코드는 이미 방출된(emit)된 요소들을 동기적으로 확인하는 방법일 뿐이다.

  • 실질적인 비동기 오퍼레이션을 테스트 하기 위해서는 테스트코드를 조금 더 작성해야한다.

  • 밑의 예제는 백그라운드 스레드 내에서 동시성 스케쥴러를 사용할 것이다.

let scheduler = ConcurrentDispatchQueueScheduler(qos: .background)
  • Observable을 생성하고 위의 스케쥴러를 구독할 것이다.
let scheduler = ConcurrentDispatchQueueScheduler(qos: .background)
let intObservable = Observable.of(10, 20, 30)
                    .map { $0 * 2 }
                    .subscribe(on: scheduler)
  • 해당 내용을 토대로 지난 테스트코드에서 작성했던 do-catch문을 다시 작성하고 Observable타입에 toBlocking메서드를 호출할 것이다.
do {
    let result = try intObservable.observe(on: MainScheduler.instance).toBlocking().toArray()
    print(result)
    XCTAssertEqual([20, 40, 60], result)
} catch {
    XCTFail("Error")
}

적용해보기

  • 지금까지 작성한 내용은 맨 위의 참고자료를 바탕으로 공부한 내용입니다.
  • 위 내용을 바탕으로 Rx로 작성한 네트워크 코드를 테스트하는 코드를 작성해 볼 것입니다.
import RxSwift
import Foundation

final class NetworkProvider {
    private var session: URLSession

    init(session: URLSession = .shared) {
        self.session = session
    }

    func request<R: Requestable>(by type: Decodable.Type, with endPoint: R, _ completion: @escaping ((Result<Decodable, Error>) -> Void)) throws {
        do {
            let request = try endPoint.makeUrlRequest(httpMethod: .GET)

            session.dataTask(with: request) { [weak self] data, response, error in
                self?.checkError(data, response, error) { result in
                    switch result {
                    case .success(let data):
                        do {
                            let decodingData = try Decoder().decodingJson(data, by: type)

                            completion(.success(decodingData))
                        } catch {
                            completion(.failure(error))
                        }
                    case .failure(let error):
                        completion(.failure(error))
                    }
                }
            }.resume()
        } catch let error {
            completion(.failure(error))
        }
    }

    func requestByRx<R: Requestable>(by type: Decodable.Type, with endPoint: R) -> Observable<Decodable> {
        return Observable.create { [weak self] observer in
            do {
                try self?.request(by: type, with: endPoint) { result in
                    switch result {
                    case .success(let data):
                        observer.onNext(data)
                        observer.onCompleted()
                    case .failure(let error):
                        observer.onError(error)
                    }
                }
            } catch {
                observer.onError(error)
            }

            return Disposables.create()
        }
    }

    private func checkError(_ data: Data?, _ response: URLResponse?, _ error: Error?, _ completion: @escaping ((Result<Data, Error>) -> Void)) {
        if let error = error {
            completion(.failure(error))
            return
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            completion(.failure(NetworkError.unknownError))
            return
        }

        guard (200...299).contains(httpResponse.statusCode) else {
            completion(.failure(NetworkError.invalidResponseError(statusCode: httpResponse.statusCode)))
            return
        }

        guard let data = data else {
            completion(.failure(NetworkError.emptyDataError))
            return
        }

        completion(.success(data))
    }
}
  • 여기서 테스트 할 메서드는 requestByRx이며, ObservableConcurrentDispatchQueueSchedulertoBlocking을 사용하여 테스트 코드를 작성할 것입니다.

Given

let fakeRequestable = FakeRequestable()
let expectationRepositoryCount = 3
let expectationFirstFullName = "fatherLeon/baekjoon"
let scheduler = ConcurrentDispatchQueueScheduler(qos: .background)
  • 우리가 확인할 것은 2가지로 DummyData의 count가 3인지, 첫번 째 요소의 fullName이 “fatherLeon/baekjoon”인지입니다.
  • 스케줄러의 경우 위 예제와 똑같이 작성하였습니다.
MockURLProtocol.requestHandler = { request in
    let response = HTTPURLResponse(url: fakeRequestable.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
    let data = DummyNetworkData.repositoryData.data(using: .utf8)!

    return (response, data)
}
  • 네트워크 통신을 하기 위해서는 URLProtocol을 상속받아 Mock객체를 만들 필요가 있는데 해당 객체 내에서 responsedata를 임의로 줄 수 있기 때문에 Dummy데이터를 넣어주었습니다.

When

let observable = networkProvider.requestByRx(by: GitRepositories.self, with: fakeRequestable)
    .map({ decodable in
        return decodable as! GitRepositories
    })
    .subscribe(on: scheduler)
  • observable을 생성했으며 반환값이 Decodable이므로 map 오퍼레이터를 활용하여 GitRepositories로 변환했습니다.
  • 이후 이전에 만들었던 스케쥴러를 구독하게끔 구현하였습니다.

Then

do {
    let result = try observable.toBlocking().single()
    let resultRepositoryCount = result.count
    let resultFirstFullName = result.first!.fullName

    XCTAssertEqual(expectationRepositoryCount, resultRepositoryCount)
    XCTAssertEqual(expectationFirstFullName, resultFirstFullName)
} catch {
    XCTFail("test_올바른_데이터_입력시_Data방출확인 - 성공해야하만 하는 테스트케이스")
}
  • do-catch문을 사용하였고 error발생시 실패한 테스트케이스이므로 XCTFail을 작성하였습니다.
  • 그리고 observabletoBlocking메서드를 활용할 경우 위 참고문서에 나오는 toArray, first, last이외에도 single메서드가 존재하는데 이는 곧바로 데이터를 가져올 수 있습니다.
  • single객체를 사용하여 결과 값을 받고 GitRepositories는 배열 값이므로 해당 배열의 count와 첫번째 배열 값의 fullName을 받아 비교하는 과정을 거쳤습니다.
반응형