[SwiftUI Mater] #17 Unit Testing

Woozoo·2023년 4월 5일
0

[SwiftUI Review]

목록 보기
32/41
post-custom-banner

Unit Tests가 UI보다는 더 중요하다!


요런 뷰랑 뷰모델이 있다고 해봅시다

이걸 가지고 테스트를 진행할 건데
보통 엑코 프로젝트 만들 때 UnitTests 체크하는 항목이 있음!
지금은 안 만들고 시작했잖음

지금만들어져 있는 파일들은 전부 Targets가 되는 앱이 정해져 있음

이럴 때 어떻게 Tests를 추가해줄 수 있을까?




요렇게!!!
새로운 Target을 추가해주면 됩니다~!


그러고 Product Name을 지정하고 완료하면됨!
(일반적으로는 기본 target 앱이름에 Tests를 붙인다! 대시바를 넣어줘도됨)

새로운 폴더가 생겼죠?

폴더 안에 있는 Tests파일을 보면 요런 마름모 버튼이 있는 걸 볼 수 있음!

요런 파일입니다!!
아래에 있는 두 메소드에서 테스트를 진행하게 되고 그 위의 두 override 메소드는 테스트 전에 셋업 같은 느낌


UnitTesting 뷰모델 만든 거 미믹해서 Tests 타겟에 파일 추가해봅시다

파일 이름은 똑같게하는데 _Tests 붙여서 만들어줌!

Tests에선 메소드의 Naming이 중요합니다!



원래읜 target을 @testable이라는 키워드와 함께 임포트 해주고


어떤 값으로 어떤 때에 그리고 그후에 어떻게 해줄 건지 작성해준 뒤에 플레이버튼 누르면~!



!으로 바꾼뒤에 실행하면!

vm.isPremium이 True로 나올거라고 assert 했는데 그렇지 않아서
에러가 발생했음!!



참 이번에 테스트하는 동안에는 중간에 있는 형태의 네이밍으로 메소드를 작성해줄거!

shouldBeFalse로도 테스트 해봤다!
잘됨



근데 보통 테스트하게 될 때 특정한 값을 지정하고 테스트한다기보다는 랜덤한 값을 가지고 테스트하게 되잖음
그럴 때 요런 식으로 구성해주는거임!
그리고 값이 같은지를 비교해서 테스트의 성공 여부를 체크하고!


근데 또 지금처럼 랜덤할 한 값을 한번만 체크하면 이게 false일 때도 잘 된 건지 모르잖음
그 때는 루프를 돌려서 체크하는겨

마름모 플레이모양이 안나오면 커맨드 b로 빌드해주자 그럼 뜬다



dataArray를 만들어봅시다


그리구 나서 dataArray가 비어 있는지 체크해봅시다!

XCTAssertEqual(vm.dataArray.count, 0)
이렇게도 가능하겠죠


그리고 뷰모델에 dataArray에 아이템추가해주는 메소드 작성


edge case추가해봅시다


새로운 테스트로 바꿔줘야겠죠



만약에 랜덤하게 확인해보고 싶다면 UUID().uuidString같이 랜덤하게 넣어줘도 되고 따로 random한 string값을 넣어줄 수 있게 메소드 구성해줘도됨!



selectItem이라는 메소드를 구성해주고 들어온 item이 dataArray에 존재하는지 체크한 뒤에 selectedItem에 값을 넣어줌


테스트로 돌아와서 시작할 때 nil인 지 체크하는 테스트 작성해줌!
AssertNil 사용해도 됩니다~

func test_UnitTestingBootcampViewModel_selectedItem_shouldBeSelected_stress() {
    // Given
    let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    
    // When
    let loopCount: Int = Int.random(in: 1..<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 saveItem(item: String) throws {
    guard !item.isEmpty else {
        throw DataError.noData
    }
    
    if let x = dataArray.first(where: { $0 == item }) {
        print("Save item here!!! \(x)")
    } else {
        throw DataError.itemNotFound
    }
    
}

enum DataError: LocalizedError {
    case noData
    case itemNotFound
}

뷰모델에 다른 메소드도 추가해봅시다
saveItem이라는 메소드를 만들고 에러가 발생할 때 DataError 이넘 타입을 throw해줬음

func test_UnitTestingBootcampViewModel_saveItem_shouldThrowError_noData() {
    // Given
    let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    
    // When
    
    // 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? UnitTestingBootcampViewModel.DataError
        XCTAssertEqual(returnedError, UnitTestingBootcampViewModel.DataError.itemNotFound)
    }
}

이렇게 테스트에서도 Throw를 잘 하는지 체크도 가능함!!


func test_UnitTestingBootcampViewModel_saveItem_shouldSaveItem() {
    // Given
    let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    
    // When
    let loopCount: Int = Int.random(in: 1..<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))
    
}


요렇게 처음에 viewModel 만들어놓고 setUp메소드에서 인스턴스 만든 다음에 viewModel 만들어진거 쓰는 것도 가능!

근데 가드문으로 한번 옵셔널 해제해줘야함!



tearDownWithError 메소드는 하나의 테스트가 끝나고 나서 리셋해줄 때 쓴다고 생각하면됨!

그럼 SetUP 하고, 테스트하고 , tearDown메소드로 리셋!

요렇게 흘러가게됨


protocol NewDataServiceProtocol {
    func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> ())
    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]) -> ()) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            completion(self.items)
        }
    }
    
    func downloadItemsWithCombine() -> AnyPublisher<[String], Error> {
        Just(items)
            .tryMap({ publishedItems in
                guard !publishedItems.isEmpty else {
                    throw URLError(.badServerResponse)
                }
                return publishedItems
            })
            .eraseToAnyPublisher()
    }
    
}

네트워킹같은 느낌으로 Combine 테스트하는 것도 해봅시다

뷰모델로 돌아와서 프로토콜을 채택해준 목데이터 서비스를 만들어줌

그리고 dataService라는 프로토콜을 채택하는 애들이 들어올 수 있게 dependency Injection 구성해줌


그리고 뷰모델 안에서 dataService의 escaping메소드를 호출하는 메소드도 작성해주고!


이 메소드를 테스트에서 실행해보면
Fail이 됩니다

왜냐! 다운로드 function에서 3초 뒤에 추가가 되고 있기 때문이죠

func test_UnitTestingBootcampViewModel_downloadWithEscaping_shouldReturnItems() {
    // Given
    let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    
    // When
    let expection = XCTestExpectation(description: "Should Return items after 3 seconds")
    
    vm.$dataArray
        .dropFirst()
        .sink { returnedItems in
            expection.fulfill()
        }
        .store(in: &cancellables)
    
    vm.downloadWithEscaping()
    
    // Then
    wait(for: [expection], timeout: 5)
    XCTAssertGreaterThan(vm.dataArray.count, 0)
}

요렇게 콤바인을 사용해서 테스트를 진행하면 됩니다
dataArray가 값을 뱉어내게 만들고 expection을 fulfill해줍니다
그리고 기다린 후에 테스트 경우를 체크!

콤바인 메소드로도 한번 테스트해봅시다


지금 NewMockDataService에 대한 테스트니까 새로 테스트 파일 만들어주는 게 낫겠죠


profile
우주형
post-custom-banner

0개의 댓글