2023. 11. 28. 15:39ㆍSwift/RxSwift
How to run tests in RxSwift
- 아래 내용은 밑의 참고 링크를 일부 번역하고 약간 변형을 주어 공부한 내용입니다.
- 번역에 오류가 있거나 이해한 것이 틀릴 수 있습니다.
RxTest와 RxBlocking의 경우 RxSwift 레포지토리 중 일부이지만 분리 되어있기 때문에 따로 불러 올 필요가 있습니다.
SPM을 사용했을 경우 RxSwift와 RxCocoa만을 import했다면 추가적으로 불러와서 사용해야합니다.
RxTest 의 경우 Rx코드를 테스팅 하는데 매우 유용한 추가기능을 제공합니다. 특히,
TestScheduler
의 경우 가상의 시간을 설정하여 특정 시간마다 이벤트를 발생 시킬 수 있습니다.RxBlocking 의 경우 일반적인
Observable
시퀀스를 변환하여 관찰 가능하도록 만듭니다.이는, 관찰 가능한 시퀀스로 변환되거나 일정 시간을 초과할 때까지 실행 중인 스레드를 차단하는데, 이로 인해 비동기 작업을 훨씬 쉽게 테스트할 수 있습니다.
RxTest
- Rx를 테스트 하기 전
RxTest
에 대해 알아야할 것이 있습니다. - RxTest는 테스트 목적으로 두 가지
Observable
이 존재합니다.- HotObservables
- 구독 여부에 상관없이 특정 시간에 정해진 이벤트를 발생시킨다.
- ColdObservables
- 일반적으로 사용한
Observables
와 같이 새로운 구독이 발생한다면 그 때 새로운 요소를 구독자에게 요청한다.
- 일반적으로 사용한
- HotObservables
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
이며,Observable
을ConcurrentDispatchQueueScheduler
과toBlocking
을 사용하여 테스트 코드를 작성할 것입니다.
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
객체를 만들 필요가 있는데 해당 객체 내에서response
와data
를 임의로 줄 수 있기 때문에 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
을 작성하였습니다. - 그리고
observable
을toBlocking
메서드를 활용할 경우 위 참고문서에 나오는toArray
,first
,last
이외에도single
메서드가 존재하는데 이는 곧바로 데이터를 가져올 수 있습니다. - single객체를 사용하여 결과 값을 받고
GitRepositories
는 배열 값이므로 해당 배열의 count와 첫번째 배열 값의fullName
을 받아 비교하는 과정을 거쳤습니다.
'Swift > RxSwift' 카테고리의 다른 글
Creating your own Observable (aka observable sequence) && Creating an Observable that performs work (0) | 2023.09.08 |
---|---|
Disposing && Implicit Observable guarantees (0) | 2023.09.07 |
Observables aka Sequences (0) | 2023.09.05 |
RxSwift 왜 사용할까? (0) | 2023.08.31 |