Combine framework tutorial - Part 4 - How to include unit testing and dependency injection
import Foundation
import Combine
import UIKit
@testable import CombineMapBootCamp
struct APIMockResources: DataProvider {
var result: Result<Data, APIResources.APIError>
func fetch(url: URL) -> AnyPublisher<Data, APIResources.APIError> {
result.publisher
.eraseToAnyPublisher()
}
}
result
를 호출하는 부분에서 필요로 하는 데이터 / 에러 부분을 호출 가능 var cancellables = Set<AnyCancellable>()
override func tearDown() {
cancellables = []
}
store
에 사용되는 변수tearDown
메소드 오버라이드 func testLoadingAlbumAtLaunch() {
let fetcher = AlbumCollectionViewModel()
XCTAssertEqual(fetcher.albumSubject.value.count, 0, "starting with no images")
let promise = expectation(description: "loading 3 images")
fetcher
.albumSubject
.sink { completion in
XCTFail()
} receiveValue: { models in
if models.count >= 10 {
promise.fulfill()
}
}
.store(in: &cancellables)
wait(for: [promise], timeout: 5)
}
url
정보에 따라 앨범을 다운로드받는 상황XCTAssertEqual
XCTFail
promise
의 fulfill()
메소드 실행wait
메소드func testLoadingMoreThanThreeImages() {
let fetcher = AlbumCollectionViewModel()
let promise = expectation(description: "loading 5 images")
fetcher
.albumSubject
.sink { models in
if models.count > 5 {
for _ in 0..<5 {
if let model = models.randomElement() {
print("Sending")
fetcher.imageUrlSubject.send(model.thumbnailUrl)
}
}
}
}
.store(in: &cancellables)
fetcher
.imagesSubject
.dropFirst()
.drop { images in
images.count == 0
}
.output(in: 1...3) // publishes 5 values one by one
.collect(3) // publishes 1 array of passed values
.contains(where: { images in
images.count == 3
})
// .allSatisfy({ images in
// images.count > 0
// })
.sink(receiveValue: { value in
promise.fulfill()
})
.store(in: &cancellables)
wait(for: [promise], timeout: 5)
}
imageUrlSubject
에 전송. 뷰 모델 이니셜라이즈 단에서 해당 퍼블리셔에 값이 들어올 경우 이미지를 다운로드 및 패치contains
부분에서 collect
를 통해 모은 여태까지의 데이터 개수가 3개인지를 확인, 이후 promise
가 실현되었는지까지 기다리는 데 총 5초 사용하는 테스트 함수. func testWhichIsErrorMessage() {
let mock = APIMockResources(result: .failure(.badResponse(statusCode: 400)))
let fetcher = AlbumCollectionViewModel(apiResource: mock)
fetcher
.albumSubject
.sink { models in
if models.count > 1 {
if let model = models.randomElement() {
print("Sending")
fetcher.imageUrlSubject.send(model.thumbnailUrl)
}
}
}
.store(in: &cancellables)
fetcher
.imagesSubject
.filter { images in
images.count > 0
}
.sink { image in
XCTFail("should not have images")
}
.store(in: &cancellables)
let promise = expectation(description: "should get error message")
fetcher
.errorMessageSubject
.filter { error in
error != nil
}
.sink { message in
promise.fulfill()
}
.store(in: &cancellables)
wait(for: [promise], timeout: 10)
}
fetch
함수를 사용하는 상황 중 실패가 일어날 때를 가정한 테스트mock
을 뷰 모델을 이니셜라이즈하는 데 사용promise
가 실현되었다고 가정, 이를 10초 동안 기다림.import XCTest
import Combine
@testable import CombineMapBootCamp
final class CombineMapBootCampTests: XCTestCase {
var cancellables = Set<AnyCancellable>()
override func tearDown() {
cancellables = []
}
func testLoadingAlbumAtLaunch() {
let fetcher = AlbumCollectionViewModel()
XCTAssertEqual(fetcher.albumSubject.value.count, 0, "starting with no images")
let promise = expectation(description: "loading 3 images")
fetcher
.albumSubject
.sink { completion in
XCTFail()
} receiveValue: { models in
if models.count >= 10 {
promise.fulfill()
}
}
.store(in: &cancellables)
wait(for: [promise], timeout: 5)
}
func testLoadingMoreThanThreeImages() {
let fetcher = AlbumCollectionViewModel()
let promise = expectation(description: "loading 5 images")
fetcher
.albumSubject
.sink { models in
if models.count > 5 {
for _ in 0..<5 {
if let model = models.randomElement() {
print("Sending")
fetcher.imageUrlSubject.send(model.thumbnailUrl)
}
}
}
}
.store(in: &cancellables)
fetcher
.imagesSubject
.dropFirst()
.drop { images in
images.count == 0
}
.output(in: 1...3) // publishes 5 values one by one
.collect(3) // publishes 1 array of passed values
.contains(where: { images in
images.count == 3
})
// .allSatisfy({ images in
// images.count > 0
// })
.sink(receiveValue: { value in
promise.fulfill()
})
.store(in: &cancellables)
wait(for: [promise], timeout: 5)
}
func testWhichIsErrorMessage() {
let mock = APIMockResources(result: .failure(.badResponse(statusCode: 400)))
let fetcher = AlbumCollectionViewModel(apiResource: mock)
fetcher
.albumSubject
.sink { models in
if models.count > 1 {
if let model = models.randomElement() {
print("Sending")
fetcher.imageUrlSubject.send(model.thumbnailUrl)
}
}
}
.store(in: &cancellables)
fetcher
.imagesSubject
.filter { images in
images.count > 0
}
.sink { image in
XCTFail("should not have images")
}
.store(in: &cancellables)
let promise = expectation(description: "should get error message")
fetcher
.errorMessageSubject
.filter { error in
error != nil
}
.sink { message in
promise.fulfill()
}
.store(in: &cancellables)
wait(for: [promise], timeout: 10)
}
}
강의에서 진행한 형식대로 뷰 모델을 구성한 게 아니기 때문에 테스트하고자 하는 대상은 다소 달라졌는데, 최대한 '작은' 단위대로 테스트하는 게 원칙!