요즘 여기저기서 많이 들리는 TDD를 위해서 필요한 Unit Test에 대해 알아보자.
TDD란?
Test-Driven Development. 소프트웨어 개발 방법론 중 하나.
테스트 코드를 먼저 작성하고 그에 맞게 실제 코드를 구현하는 방식.
TDD는 아래와 같은 순서로 진행된다.
1. 실패하는 테스트 코드 작성(요구사항 충족하지 않는 테스트 케이스 작성)
2. 테스트를 통과하는 최소한의 코드 작성
3. 테스트 통과 확인
4. 리팩토링(작성한 코드 개선, 중복 제거 및 품질 향상)
위 과정을 반복하면서 요구사항을 충족하는 코드를 점점 디벨롭하는 것.
Unit Test는 이런 개발 사이클에서 핵심적인 역할을 담당한다.
Unit Test
Unit Test는 작은 단위의 코드, 즉 함수나 모듈 등의 개별적인 요소에 대해 '독립적으로' 테스트 수행하는 것을 말한다.
소스 코드의 일부분을 유닛이라고 부름.
-> 코드의 동작을 확인하고, 예상치 못한 버그를 사전에 발견할 수 있다.
Unit Test를 작성하면 코드의 신뢰도가 올라가고, 코드에 변경사항이 생겼을 때 기존 기능이 올바르게 작동하는지
빠르게 확인할 수 있다.
Unit Test를 하는 이유
1. 버그 감소
- 코드의 예상되는 동작과 실제 동작 사이의 차이를 찾아내는 데 도움을 줌.
테스트를 통해 버그를 발견하고 수정할 수 있음 -> 버그 감소 및 코드 품질 향상
2. 리팩토링의 안정성 확보
리팩토링은 코드를 개선하고, 유지 보수가 쉽게 만드는 과정을 말한다.
Unit Testing을 하면 코드가 변경되었을 때 기존 기능에 영향을 미치는 지 빠르게 확인할 수 있기 때문에
리팩토링 괒어을 안전하게 진행할 수 있다.
3. 문서화 역할
Unit Testing은 코드의 예상 동작을 명시적으로 정의하는 테스트 케이스를 작성한다.
이는 코드의 사용 방법을 문서화 하는 역할을 하기도 하며, 코드 이해에 도움이 될 수 있다.
Unit Test의 장점
1. 신뢰성 향상
2. 유지 보수 용이성
3. 개발 속도 향상
- 버그를 사전에 발견하고 수정할 수 있기 때문. 개발이 많이 진행되고 나서 버그를 수정하는 것은 상대적으로 어렵기 때문.
4. 코드 설계 개선
- 코드를 모듈화하고, 의존성을 낮추는 좋은 설계 원칙을 적용할 수 있도록 도와줌.
- ex.테스터블한 코드 작성을 위해서 의존성 주입 등을 활용
Unit Test의 단점
1. 시간과 비용
- 테스트 코드 작성에 추가적인 시간과 비용이 필요하다.
유닛 테스트를 작성하기 위해서 기존 코드를 변경해야 할 수도 있다.
2. 테스트 커버리지
모든 코드에 대해 테스트 케이스를 작성하지 않는 한, 테스트 커버리지를 100%로 달성하는 것은 현실적으로 어렵다.
3.변경에 대한 유연성
테스트가 많은 경우, 코드의 변경에 따라 테스트 케이스를 수정해야 할 수도 있다.
이는 코드 변경에 따른 추가 작업이 요구되기 때문에 유연성을 제한할 수 있다.
XCTEST
Swift에서 Unit Testing을 수행하기 위해 XCTEST 프레임워크를 사용할 수 있다.
XCTest 프레임워크를 사용해서 테스트 케이스 클래스를 작성하고,
각 테스트 메서드에서 코드의 예상 동작을 확인할 수 있다.
Unit Test 시작하기
Xcode 상단의 File - New - Target을 통해
테스트 타겟을 정하고, Unit Testing Bundle을 만들어 준다.
Xcode의 좌측 네비게이터에서 New File을 추가해 준다.
여기서 우리는 Unit Test를 만들 것이기 때문에 Unit Test Case Class를 선택한다.
파일을 생성하면 자동으로 아래와 같은 샘플 코드가 만들어진다.
// Created by Toughie on 2023/07/01.
//
import XCTest
// XCTestCase를 상속받은 클래스 생성
final class myTest2: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// 테스트 메서드 실행 전에 호출되는 메서드.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
// 각 테스트 메서드 실행 후 정리 코드를 작성.
// ex. 테스트 실행 전 필요한 초기화 작업을 여기서 수행
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
// 테스트할 코드를 작성하고, XCTAssert와 관련 함수를 사용해서 예상 결과를 확인.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
// 코드의 실행 시간을 축정해서 성능을 테스트
}
}
// 테스트 메서드는 여러개 작성 가능하며 각 테스트 메서드는 독립적으로 실행됨.
}
테스트 타겟 코드
간단한 뷰, 뷰모델, 모델(데이터서비스)로 구성되어 있다.
Model
import SwiftUI
import Combine
// 의존성 주입을 위한 프로토콜.
// 비동기 작업을 @escaping, combine 두 가지 방법으로 진행해봄.
protocol NewDataServiceProtocol {
func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> Void)
func downloadItemsWithCombine() -> AnyPublisher<[String], Error>
}
class NewMockDataService: NewDataServiceProtocol {
let items: [String]
//item을 입력하지 않으면 nil 코얼레싱을 통해 디폴트 아이템 입력
init(items: [String]?) {
self.items = items ?? [
"one",
"two",
"three"
]
}
//escaping 클로저를 활용(completion handler)를 통해 비동기 작업을 한다고 가정(3초 걸림)
func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion(self.items)
}
}
// 컴바인을 활용해서 다운로드 상황 가정
// 다운로드에 실패하면 URLError을 던짐.
func downloadItemsWithCombine() -> AnyPublisher<[String], Error> {
Just(items)
.tryMap({ publishedItems in
guard !publishedItems.isEmpty else {
throw URLError(.badServerResponse)
}
return publishedItems
})
.eraseToAnyPublisher()
}
}
View
import SwiftUI
struct UnitTestingPracView: View {
@StateObject private var vm: UnitTestingPracViewModel
//뷰모델 주입
init(isPremium: Bool) {
_vm = StateObject(wrappedValue: UnitTestingPracViewModel(isPremium: isPremium))
}
var body: some View {
Text(vm.isPremium.description)
}
}
ViewModel
import SwiftUI
import Combine
class UnitTestingPracViewModel: ObservableObject {
@Published var isPremium: Bool
@Published var dataArray: [String] = []
@Published var selectedItem: String?
let dataService: NewDataServiceProtocol
var cancellables = Set<AnyCancellable>()
//편의를 위해 default parameter로 NewMockDataService(items: nil) 사용
init(isPremium: Bool, dataService: NewDataServiceProtocol = NewMockDataService(items: nil)) {
self.isPremium = isPremium
self.dataService = dataService
}
//빈문자열이 아닌 경우, 배열에 추가하는 메서드
func addItem(item: String) {
guard !item.isEmpty else { return }
self.dataArray.append(item)
}
// 배열에 해당 값이 존재하면 selectedItem에 할당
// 그렇지 않다면 selectedItem을 nil로 초기화.
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("Item Saved: \(x)")
} else {
throw DataError.itemNotFound
}
}
//에러 처리를 위한 열거형
enum DataError: LocalizedError {
case noData
case itemNotFound
}
// escaping closure를 통한 데이터 다운로드
// 강한 순환 참조 방지를 위해 [weak self] 활용
func downloadWithEscaping() {
dataService.downloadItemsWithEscaping { [weak self] returnedItems in
self?.dataArray = returnedItems
}
}
// 컴바인을 활용한 데이터 다운로드
func downloadWithCombine() {
dataService.downloadItemsWithCombine()
.sink { _ in
//deal with error
} receiveValue: { [weak self] returnedItems in
self?.dataArray = returnedItems
}
.store(in: &cancellables)
}
}
테스트 케이스 작성
테스트 케이스 이름 작성법
test_유닛이름(구조체, 클래스 등)_유닛내부(프로퍼티, 메서드 등)_예상동작
맨 앞에 test는 꼭 붙여줘야 테스트를 실행할 수 있다.
테스트 케이스 구조
1. Given(주어진 상황)
테스트 케이스에서 테스트할 상황을 설정하는 부분.
테스트에 필요한 초기 조건을 설정하고 입력값을 준비함.
테스트 환경 설정 or 객체를 생성하는 등의 작업을 수행함.
2. When(동작)
테스트할 동작이나 메서드를 호출하는 부분.
테스트 대상이 되는 메서드나 특정 동작을 수행하고 그 결과를 확인.
3. Then(결과 확인)
테스트의 예상 결과를 확인하는 부분.
특정 조건이나 기대 결과를 확인해서 테스트 성공 여부를 결정
예상한 결과와 실제 결과를 비교함.
XCTAssert 메서드를 활용해서 특정 조건을 확인하고, 테스트의 성공 여부 판단함.
주로 사용되는 XCTAssert의 종류는 더보기에 정리되어 있다.
사실 이름을 보면 뭘 검증하려는 지 바로 알 수 있다.
- XCTAssertEqual: 두 값이 서로 동일한지 확인. 예상 값과 실제 값이 같은 경우 테스트를 통과하고, 다른 경우에는 실패.
- XCTAssertNotEqual: 두 값이 서로 다른지 확인. 예상 값과 실제 값이 다른 경우 테스트를 통과하고, 같은 경우에는 실패.
- XCTAssertTrue: 조건이 true인지 확인. 주어진 조건이 true인 경우 테스트를 통과하고, false인 경우에는 실패.
- XCTAssertFalse: 조건이 false인지 확인. 주어진 조건이 false인 경우 테스트를 통과하고, true인 경우에는 실패.
- XCTAssertNil: 값이 nil인지 확인. 값이 nil인 경우 테스트를 통과하고, nil이 아닌 경우에는 실패.
- XCTAssertNotNil: 값이 nil이 아닌지 확인. 값이 nil이 아닌 경우 테스트를 통과하고, nil인 경우에는 실패.
- XCTAssertThrowsError: 특정 코드 블록에서 에러가 발생하는지 확인. 코드 블록에서 에러가 발생하면 테스트를 통과하고, 에러가 발생하지 않는 경우에는 실패.
- XCTAssertNoThrow: 특정 코드 블록에서 에러가 발생하지 않는지 확인. 코드 블록에서 에러가 발생하지 않으면 테스트를 통과하고, 에러가 발생하는 경우에는 실패.
- XCTAssertGreaterThan: 첫 번째 값이 두 번째 값보다 큰지 확인. 큰 경우 테스트를 통과하고, 작거나 같은 경우에는 실패.
- XCTAssertLessThan: 첫 번째 값이 두 번째 값보다 작은지 확인. 작은 경우 테스트를 통과하고, 크거나 같은 경우에는 실패.
- XCTAssertGreaterThanOrEqual: 첫 번째 값이 두 번째 값보다 크거나 같은지 확인. 크거나 같은 경우 테스트를 통과하고, 작은 경우에는 실패.
- XCTAssertLessThanOrEqual: 첫 번째 값이 두 번째 값보다 작거나 같은지 확인. 작거나 같은 경우 테스트를 통과하고, 큰 경우에는 실패.
Model(데이터 서비스) 테스트
import XCTest
import Combine
// @testable 어트리뷰트를 통해 테스트 대상 모듈의 내부 요소에 접근할 수 있도록.
@testable import SwiftUI_Advanced_
final class NewMockDataService_Tests: XCTestCase {
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
//테스트가 끝나면 매번 cancellables를 초기화 해줌.
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
cancellables.removeAll()
}
// 데이터 서비스 초기화 시 nil, 빈배열, 값을 전달 했을 때 정확히 초기화 되는가?
func test_NewMockDataService_init_doesSetValuesCorrectly() {
// Given
let items: [String]? = nil
let items2: [String]? = []
let items3: [String]? = [UUID().uuidString, UUID().uuidString]
// When
let dataService = NewMockDataService(items: items)
let dataService2 = NewMockDataService(items: items2)
let dataService3 = NewMockDataService(items: items3)
// Then
XCTAssertFalse(dataService.items.isEmpty)
XCTAssertTrue(dataService2.items.isEmpty)
XCTAssertEqual(dataService3.items.count, items3?.count)
}
func test_NewMockDataService_downloadItemsWithEscaping_doesReturnValues() {
// Given
let dataService = NewMockDataService(items: nil)
// When
var items: [String] = []
// 비동기적인 테스트에서 사용.
let expectation = XCTestExpectation()
dataService.downloadItemsWithEscaping { returnedItems in
items = returnedItems
expectation.fulfill()
}
// Then
wait(for: [expectation], timeout: 5)
// 비동기 작업이 완료될 때까지 테스트를 대기시키고
// 특정 조건이 충족될 때까지 테스트를 일시 중단.
// expectation이 fulfill 상태가 될 때 까지 대기함.
XCTAssertEqual(items.count, dataService.items.count)
}
func test_NewMockDataService_downloadItemsWithCombine_doesReturnValues() {
// Given
let dataService = NewMockDataService(items: nil)
// When
var items: [String] = []
let expectation = XCTestExpectation()
dataService.downloadItemsWithCombine()
.sink { completion in
switch completion {
case .finished:
expectation.fulfill()
case .failure:
XCTFail()
}
} receiveValue: { returnedItems in
items = returnedItems
}
.store(in: &cancellables)
// Then
wait(for: [expectation], timeout: 5)
XCTAssertEqual(items.count, dataService.items.count)
}
func test_NewMockDataService_downloadItemsWithCombine_doesFail() {
// Given
let dataService = NewMockDataService(items: [])
// When
var items: [String] = []
let expectation = XCTestExpectation(description: "Does throw an error")
let expectation2 = XCTestExpectation(description: "Does throw URLError.badServerResponse")
dataService.downloadItemsWithCombine()
.sink { completion in
switch completion {
case .finished:
XCTFail()
case .failure(let error):
//아이템이 빈 배열이니까 에러를 던짐.
expectation.fulfill()
// let urlError = error as? URLError
// // same type
// XCTAssertEqual(urlError, URLError(.badServerResponse))
if error as? URLError == URLError(.badServerResponse) {
expectation2.fulfill()
}
}
} receiveValue: { returnedItems in
items = returnedItems
}
.store(in: &cancellables)
// Then
wait(for: [expectation, expectation2], timeout: 5)
XCTAssertEqual(items.count, dataService.items.count)
}
}
ViewModel 테스트
import XCTest
import Combine
@testable import SwiftUI_Advanced_
final class UnitTestingPracViewModel_Test: XCTestCase {
//셋업 단게에서 뷰모델을 초기화 해주기 위해
var viewModel: UnitTestingPracViewModel?
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
viewModel = UnitTestingPracViewModel(isPremium: Bool.random())
}
//테스트 끝날 때마다 리셋 - 뷰모델 nil로 초기화
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
viewModel = nil
}
func test_UnitTestingPracViewModel_isPremium_shouldBeTrue() {
// Given
let userIsPremium: Bool = true
// When
let vm = UnitTestingPracViewModel(isPremium: userIsPremium)
// Then
XCTAssertTrue(vm.isPremium)
}
func test_UnitTestingPracViewModel_isPremium_shouldBeFalse() {
// Given
let userIsPremium: Bool = false
// When
let vm = UnitTestingPracViewModel(isPremium: userIsPremium)
// Then
XCTAssertFalse(vm.isPremium)
}
func test_UnitTestingPracViewModel_isPremium_shouldBeInjectedValue() {
// Given
let userIsPremium: Bool = Bool.random()
// When
let vm = UnitTestingPracViewModel(isPremium: userIsPremium)
// Then
XCTAssertEqual(vm.isPremium, userIsPremium)
}
//값을 매번 할당해서 전수 조사할 수 없으니, 루프를 통해 테스트(스트레스 테스트)
func test_UnitTestingPracViewModel_isPremium_shouldBeInjectedValue_stress() {
for _ in 0..<100 {
// Given
let userIsPremium: Bool = Bool.random()
// When
let vm = UnitTestingPracViewModel(isPremium: userIsPremium)
// Then
XCTAssertEqual(vm.isPremium, userIsPremium)
}
}
func test_UnitTestingPracViewModel_dataArray_shouldbeEmpty() {
// Given
// When
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// Then
XCTAssertTrue(vm.dataArray.isEmpty)
XCTAssertEqual(vm.dataArray.count, 0)
}
func test_UnitTestingPracViewModel_dataArray_shouldAddItems() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let loopCount: Int = Int.random(in: 1..<100)
for _ in 0..<loopCount {
vm.addItem(item: UUID().uuidString) //max 36
}
// Then
XCTAssertTrue(!vm.dataArray.isEmpty)
XCTAssertFalse(vm.dataArray.isEmpty)
XCTAssertEqual(vm.dataArray.count, loopCount)
XCTAssertNotEqual(vm.dataArray.count, 0)
XCTAssertGreaterThan(vm.dataArray.count, 0)
}
//빈문자열의 경우 가드문에서 걸림.
func test_UnitTestingPracViewModel_dataArray_shouldNotAddBlankString() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
vm.addItem(item: "")
// Then
XCTAssertTrue(vm.dataArray.isEmpty)
}
//테스트 클래스 내부에서 초기화 해 둔 뷰모델을 활용하는 경우
func test_UnitTestingPracViewModel_dataArray_shouldNotAddBlankString2() {
// Given
guard let vm = viewModel else {
XCTFail()
return
}
// When
vm.addItem(item: "")
// Then
XCTAssertTrue(vm.dataArray.isEmpty)
}
func test_UnitTestingPracViewModel_selectedItem_shouldStartAsNil() {
// Given
// When
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// Then
XCTAssertTrue(vm.selectedItem == nil)
XCTAssertNil(vm.selectedItem)
}
func test_UnitTestingPracViewModel_selectedItem_shouldBeNilWhenSelectingInvalidItem() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let newItem = UUID().uuidString
vm.addItem(item: newItem)
vm.selectItem(item: newItem)
//newItem과 UUID().uuidString은 다르기 때문에 selectedItem이 nil이 될 것
vm.selectItem(item: UUID().uuidString)
// Then
XCTAssertNil(vm.selectedItem)
}
func test_UnitTestingPracViewModel_selectedItem_shouldBeSelected() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let newItem = UUID().uuidString
vm.addItem(item: newItem)
vm.selectItem(item: newItem)
// Then
XCTAssertNotNil(vm.selectedItem)
XCTAssertEqual(newItem, vm.selectedItem)
}
func test_UnitTestingPracViewModel_selectedItem_shouldBeSelected_stress() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let loopCount: Int = Int.random(in: 0..<100)
var itemsArray: [String] = []
for _ in 0..<loopCount {
let newItem = UUID().uuidString
vm.addItem(item: newItem)
itemsArray.append(newItem)
}
let randomItem = itemsArray.randomElement() ?? ""
vm.selectItem(item: randomItem)
// Then
XCTAssertNotNil(vm.selectedItem)
XCTAssertEqual(vm.selectedItem, randomItem)
}
func test_UnitTestingPracViewModel_saveItem_shouldThrowError_itemNotFound() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let loopCount: Int = Int.random(in: 0..<100)
for _ in 0..<loopCount {
vm.addItem(item: UUID().uuidString)
}
// Then
XCTAssertThrowsError(try vm.saveItem(item: UUID().uuidString))
XCTAssertThrowsError(try vm.saveItem(item: UUID().uuidString), "Should throw Item Not Found error") { error in
let returnedError = error as? UnitTestingPracViewModel.DataError
XCTAssertEqual(returnedError, UnitTestingPracViewModel.DataError.itemNotFound)
}
}
func test_UnitTestingPracViewModel_saveItem_shouldThrowError_noData() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let loopCount: Int = Int.random(in: 0..<100)
for _ in 0..<loopCount {
vm.addItem(item: UUID().uuidString)
}
// Then
do {
try vm.saveItem(item: "")
} catch let error {
let returnedError = error as? UnitTestingPracViewModel.DataError
XCTAssertEqual(returnedError, UnitTestingPracViewModel.DataError.noData)
}
}
func test_UnitTestingPracViewModel_saveItem_shouldSaveItem() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let loopCount: Int = Int.random(in: 0..<100)
var itemsArray: [String] = []
for _ in 0..<loopCount {
let newItem = UUID().uuidString
vm.addItem(item: newItem)
itemsArray.append(newItem)
}
let randomItem = itemsArray.randomElement() ?? ""
XCTAssertFalse(randomItem.isEmpty)
// Then
XCTAssertNoThrow(try vm.saveItem(item: randomItem))
do {
try vm.saveItem(item: randomItem)
} catch {
XCTFail()
}
}
func test_UnitTestingPracViewModel_downloadWithEscaping_shouldReturnItems() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let expectation = XCTestExpectation(description: "Should return items after 3 seconds")
//subscribe publisher
// Publisher 배열 구독, 처음에는 빈 배열이기 때문에
// .dropFirst()를 통해 버려줌.
// 스트림에서 첫 번째 요소를 제외한 나머지 요소들을 반환
vm.$dataArray
.dropFirst()
.sink { returnedItems in
expectation.fulfill()
}
.store(in: &cancellables)
vm.downloadWithEscaping()
// Then
wait(for: [expectation], timeout: 5)
XCTAssertGreaterThan(vm.dataArray.count, 0)
}
func test_UnitTestingPracViewModel_downloadWithCombine_shouldReturnItems() {
// Given
let vm = UnitTestingPracViewModel(isPremium: Bool.random())
// When
let expectation = XCTestExpectation(description: "Should return items after a second")
//subscribe publisher
vm.$dataArray
.dropFirst()
.sink { returnedItems in
expectation.fulfill()
}
.store(in: &cancellables)
vm.downloadWithCombine()
// Then
wait(for: [expectation], timeout: 5)
XCTAssertGreaterThan(vm.dataArray.count, 0)
}
//init(isPremium: Bool, dataService: NewDataServiceProtocol = NewMockDataService(items: nil)) {
func test_UnitTestingPracViewModel_downloadWithCombine_shouldReturnItems2() {
// Given
let items: [String] = [UUID().uuidString, UUID().uuidString, UUID().uuidString, UUID().uuidString, UUID().uuidString]
let dataSerivce: NewDataServiceProtocol = NewMockDataService(items: items)
let vm = UnitTestingPracViewModel(isPremium: Bool.random(), dataService: dataSerivce)
// When
let expectation = XCTestExpectation(description: "Should return items after a second")
//subscribe publisher
vm.$dataArray
.dropFirst()
.sink { returnedItems in
expectation.fulfill()
}
.store(in: &cancellables)
vm.downloadWithCombine()
// Then
wait(for: [expectation], timeout: 5)
XCTAssertGreaterThan(vm.dataArray.count, 0)
XCTAssertEqual(vm.dataArray.count, items.count)
}
}
테스트 케이스를 작성하는 것이 시간도 많이 걸리고 쉽지는 않지만,
엣지 케이스를 사전에 생각할 수 있고, 쓰다 보면 코드의 동작 방식을 더 잘 이해할 수 있는 것 같다 :)
'iOS Developer > Swift' 카테고리의 다른 글
의존성 주입 (DI, Dependency Injection) (0) | 2023.06.18 |
---|---|
closure, @escaping, completionHandler 콜백함수 (0) | 2023.05.29 |
ARC, weak self, 캡쳐리스트 (0) | 2023.05.28 |
스위프트와 프로그래밍 패러다임 (0) | 2023.04.08 |
고차함수 ( Higher-order Function)_ map, filter, reduce (0) | 2023.04.04 |