Unit Testing a SwiftUI application in Xcode | Advanced Learning #17
import
한 유닛 테스트를 통해 확인test
UnitofWork
StateUnderTest
ExpectedBehavior
→ test_[struct or class]_[variable or function]-[expected result]
UnitTestingBootCampViewModel
클래스의 isPremium
이라는 변수 값이 참이어야 할 때, 관련 테스트를 test_UnitTestingBootCampViewModel_isPremium_ShouldBeTrue
정도로 작성할 수 있다.Given
, When
, Then
으로 구별Combine
→ 시간이 걸리는 작업이기 때문에 테스트 단위에서도 wait
를 통해 특정 작업이 끝날 때까지 '기다리는' 작업이 필요expectation
을 작성 후 sink
에서 나오는 결괏값에 따라 completion
이 finish
인지 failure
인지 알 수 있는데, 원하는 상황에 따라 Assert
를 주면 된다.import SwiftUI
import Combine
class UnitTestingBootCampViewModel: ObservableObject {
@Published var isPremium: Bool
@Published var dataArray: [String] = []
@Published var selectedItem: String? = nil
let dataService: NewDataServiceProtocol
var cancellables = Set<AnyCancellable>()
init(isPremium: Bool, dataService: NewDataServiceProtocol = NewMockDataService(items: nil)) {
self.isPremium = isPremium
self.dataService = dataService
}
func addItem(item: String) {
guard !item.isEmpty else { return }
dataArray.append(item)
}
func selectItem(item: String) {
if let x = dataArray.first(where: {$0 == item}) {
selectedItem = x
} else {
selectedItem = nil
}
}
func saveItem(item: String) throws {
guard !item.isEmpty else {
throw DataError.noData
}
if let x = dataArray.first(where: {$0 == item}) {
print("SAVED ITEM: \(x)")
} else {
print("ITEM NOT FOUND!")
throw DataError.noItemFound
}
}
func downloadWithEscaping() {
dataService.downloadItemsWithEscaping { [weak self] items in
self?.dataArray = items
}
}
func downloadWithCombine() {
dataService.downloadItemsWithCombine()
.sink { _ in
} receiveValue: { [weak self] returnedItems in
guard let self = self else { return }
self.dataArray = returnedItems
}
.store(in: &cancellables)
}
enum DataError: LocalizedError {
case noData
case noItemFound
}
}
mport SwiftUI
import Combine
protocol NewDataServiceProtocol {
func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> Void)
func downloadItemsWithCombine() -> AnyPublisher<[String], Error>
}
class NewMockDataService: NewDataServiceProtocol {
let items: [String]
init(items: [String]?) {
self.items = items ?? ["One", "Two", "Three"]
}
func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
completion(self.items)
}
}
func downloadItemsWithCombine() -> AnyPublisher<[String], Error> {
return Just(items)
.tryMap({ publishedItem in
guard !publishedItem.isEmpty else {
throw URLError(.badServerResponse)
}
return publishedItem
})
.eraseToAnyPublisher()
}
}
import XCTest
import Combine
@testable import SwiftfulThinkingAdvancedLearning
class UnitTestingBootCampViewModel_Tests: XCTestCase {
var viewModel: UnitTestingBootCampViewModel?
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
viewModel = UnitTestingBootCampViewModel(isPremium: Bool.random())
}
override func tearDownWithError() throws {
viewModel = nil
}
...
func test_UnitTestingBootCampViewModel_downloadWithCombine_shouldReturnItems2() {
// Given
let items = Array(repeating: UUID().uuidString, count: 5)
let dataService: NewDataServiceProtocol = NewMockDataService(items: items)
let viewModel = UnitTestingBootCampViewModel(isPremium: Bool.random(), dataService: dataService)
// When
let expectation = XCTestExpectation(description: "Should Return Items after a seconds")
viewModel.$dataArray
.dropFirst()
.sink { returnedItems in
// Extepctation Observer
expectation.fulfill()
}
.store(in: &cancellables)
viewModel.downloadWithCombine()
// Then
wait(for: [expectation], timeout: 1.0)
XCTAssertGreaterThan(viewModel.dataArray.count, 0)
XCTAssertEqual(viewModel.dataArray.count, items.count)
}
}
setUpWithError
를 통해 초기 세팅을 공통적으로, tearDownWithError
를 통해 초기화 과정을 해준다.