본문 바로가기
개발 이야기

[iOS앱 개발] Mock 객체와 Protocol주입을 통한 UnitTest(실전편)

by Jimmy_iOS 2023. 6. 10.

이전 포스팅에서 Mock객체와 Protocol 주입을 통한 UnitTest의 개념에 대해서 알아봤는데요. 

이번 편에서는 실제로 제 앱에서 어떤 식으로 Mock객체를 만들었는지 소개해드리겠습니다.

 

StocksDataManager 및 StocksDataManagerProtocol

 

StocksDataManager는 주식 차트 데이터를 가져 오기위한 API를 제공하는 클래스입니다. 

 

현재 앱에서는 MVVM패턴을 사용해 ViewModel이 StocksDataManager에서 가져온 데이터를 가공해 View에 그려주는 역할을 하고 있습니다.

앱을 개발할 때 StocksDataManager에서 데이터를 받아와 ViewModel이 원하는 형태로 데이터를 가공하고 있는지 매번 네트워킹을 통해 값을 확인했어야 했습니다.

 

하지만 네트워킹 없이 Mock값을 만들어 ViewModel에 전달해주고 데이터가 잘 가공되고 있는지만 확인하면 되기때문에 Mock객체를 활용한 UnitTest를 해볼 필요가 있었습니다.

 

StocksDataManagerProtocol은StocksDataManager의 API 메서드를 정의하는 프로토콜입니다.

protocol StocksDataManagerProtocol {
    func fetchChartData(
        stockSymbol: StocksSymbol,
        range: ChartRange) async -> ChartData?

    func fetchWithHanaData(
        stockSymbol: StocksSymbol,
        startOfDay: TimeInterval) async -> ChartData?

    func fetchHanaChartData(
    ) async -> Result<ChartData?, HanaAPIServiceError>
}

class StocksDataManager: StocksDataManagerProtocol {
    // StocksDataManagerProtocol에서 정의 된 API 메서드의 구현
}

MockDataManager

MockDataManager는 StocksDataManagerProtocol을 준수하는 클래스이며 API 메서드의 모의 구현을 제공합니다. MockDataManager는 실제 API 호출없이 StocksDataManager를 사용하는 코드를 테스트하는 데 유용합니다.

class MockDataManager: StocksDataManagerProtocol {
    var fetchChartDataCallCount = 0
    var fetchWithHanaDataCallCount = 0
    var fetchHanaChartDataCallCount = 0

    func fetchChartData(
        stockSymbol: StocksSymbol,
        range: ChartRange) async -> ChartData? {
        fetchChartDataCallCount += 1
        return ChartData.mockData
    }

    func fetchWithHanaData(
        stockSymbol: StocksSymbol,
        startOfDay: TimeInterval) async -> ChartData? {
        fetchWithHanaDataCallCount += 1
        return ChartData.mockData
    }

    func fetchHanaChartData() async -> Result<ChartData?, HanaAPIServiceError> {
        fetchHanaChartDataCallCount += 1
        return .success(ChartData.mockData)
    }
}

MockDataManager 클래스에서는 모의 데이터로StocksDataManagerProtocol 메서드를 구현하고 각 메서드가 호출 된 횟수를 추적하는 몇 가지 속성을 추가했습니다.

MockDataManager을 사용한 유닛 테스트

이제 StocksDataManager, StocksDataManagerProtocol  MockDataManager 클래스가 있으므로 StocksDataManager의 기능을 확인하기 위해 유닛 테스트를 작성할 수 있습니다.

import XCTest
@testable import MockTestApp

final class MockTestAppTests: XCTestCase {

    func testMockViewModel_ChartData() async throws {
        // Given
        let mockDataManager = MockDataManager()
        let mockViewModel = MainViewModel(dataManager: mockDataManager)
        let ranges: [ChartRange] =
        [.oneDay, .oneWeek, .oneMonth, .oneYear, .fiveYear]
        let symbols: [StocksSymbol] = [.dollar_Won, .dollar_Index]
        // When
        for symbol in symbols {
            for range in ranges {
                let result = await mockViewModel
                    .stocksDataManager
                    .fetchChartData(
                        stockSymbol: symbol, range: range
                    )
                //Then
                XCTAssertNotNil(result)
                XCTAssertEqual(result?.meta.previousClose, 0)
                XCTAssertEqual(result?.meta.regularMarketPrice, 0)
                XCTAssertEqual(
                    result?.indicators[0].timestamp,
                    Date(timeIntervalSince1970: 0)
                )
                XCTAssertEqual(result?.indicators[0].close, 0)
            }
        }
        XCTAssertEqual(mockDataManager.fetchChartDataCallCount, 10)
        XCTAssertEqual(mockDataManager.fetchWithHanaDataCallCount, 0)
        XCTAssertEqual(mockDataManager.fetchHanaChartDataCallCount, 0)
    }

    
    func testMockViewModel_FetchWithHanaData() async throws {
        // Given
        let mockDataManager = MockDataManager()
        let mockViewModel = MainViewModel(dataManager: mockDataManager)
        // When
        let result = await mockViewModel
            .stocksDataManager
            .fetchWithHanaData(stockSymbol: .hanaBank, startOfDay: 0)
        //Then
        XCTAssertNotNil(result)
        XCTAssertEqual(result?.meta.previousClose, 0)
        XCTAssertEqual(result?.meta.regularMarketPrice, 0)
        XCTAssertEqual(
            result?.indicators[0].timestamp,
            Date(timeIntervalSince1970: 0)
        )
        XCTAssertEqual(result?.indicators[0].close, 0)
        
        XCTAssertEqual(mockDataManager.fetchChartDataCallCount, 0)
        XCTAssertEqual(mockDataManager.fetchWithHanaDataCallCount, 1)
        XCTAssertEqual(mockDataManager.fetchHanaChartDataCallCount, 0)
    }
    
    func testMockViewModel_FetchHanaChartData() async throws {
        // Given
        var mockDataManager = MockDataManager()
        var mockViewModel = MainViewModel(dataManager: mockDataManager)
        // When
        let result = await mockViewModel
            .stocksDataManager
            .fetchHanaChartData()
        //Then
        XCTAssertNotNil(result)
        switch result {
        case .success(let chartData):
            XCTAssertEqual(chartData?.meta.previousClose, 0)
            XCTAssertEqual(chartData?.meta.regularMarketPrice, 0)
            XCTAssertEqual(chartData?.indicators[0].close, 0)
            XCTAssertEqual(
                chartData?.indicators[0].timestamp,
                Date(timeIntervalSince1970: 0)
            )
        case .failure(_): break
        }
        XCTAssertEqual(mockDataManager.fetchChartDataCallCount, 0)
        XCTAssertEqual(mockDataManager.fetchWithHanaDataCallCount, 0)
        XCTAssertEqual(mockDataManager.fetchHanaChartDataCallCount, 1)
     
    }
    
}

위의 코드는 MockTestApp 프로젝트에서 MainViewModel에서 사용하는 StocksDataManager 클래스에 대한 유닛테스트 코드입니다.

MainViewModel에서 StocksDataManager를 사용하는 데, 이를 테스트하기 위해서는 실제 API를 호출할 필요가 없습니다.

대신 MockDataManager를 사용하여 가짜 데이터를 사용하여 테스트를 수행합니다. MockTestAppTests 클래스에서 testMockViewModel_ChartData, testMockViewModel_FetchWithHanaData, testMockViewModel_FetchHanaChartData 메소드가 있습니다.

각각의 테스트 메소드에서는 StocksDataManager의 다양한 메소드를 호출하고, 반환된 값이 예상과 일치하는지 확인합니다. 이때, MockDataManager에서 fetchChartData, fetchWithHanaData, fetchHanaChartData가 호출된 횟수도 확인합니다.

 

위의 테스트 코드를 통해 개발자는 StocksDataManager 클래스의 메소드를 가짜 데이터를 사용하여 테스트할 수 있고, 버그를 더 쉽게 발견하고 수정할 수 있습니다.

 

결론

이 블로그 포스트에서는 MockDataManager를 사용하여 효과적인 유닛 테스트를 작성하는 방법을 살펴 보았습니다. StocksDataManager  StocksDataManagerProtocol 클래스를 예시로 사용하고 성공 및 실패 시나리오에 대한 테스트 케이스를 작성하는 방법을 보여주었습니다.

유닛 테스트 작성은 iOS 앱 개발의 필수 요소이며 MockDataManager을 사용하면 효과적인 유닛 테스트를 작성하기 쉬워집니다. 유닛 테스트를 통해 코드의 기능을 검증하고 개발 프로세스 초기에 버그를 식별하고 수정하는 데 도움이됩니다.