Archive/패캠(초격차)

part5 (ch1). FindCVS UnitTest 코드리뷰 (feat. Stubber)

lgvv 2022. 2. 20. 18:08

FindCVS UnitTest 코드리뷰 

 

 

✅ 개발을 완료했으면 코드리뷰를 하는건 당연.

테스트를 하면서 처음으로 XCTest와 관련한 외부 라이브러리를 사용해봐서 더욱 집중해서 보게 되었음.

 

그리고 개발자마다 다른 스타일을 갖고 있던데, 보면서 어떤 점이 더 나은지 스스로 계속 생각하게 되는 시간이라 좋았다.

 

UI테스트는 진행하지 않았음.

 

✅ 테스트코드 파일 구조

파일의 구조도

 

🟠 LocationInformationModelTest.swift

//
//  LocationInformationModelTest.swift
//  FindCVSTests
//
//  Created by Hamlit Jason on 2022/02/18.
//

import XCTest
import Nimble

@testable import FindCVS

class LocationInformationModelTest: XCTestCase {
    
    let stubNetwork = LocalNetworkStub()
    var doc: [KLDocument]!
    var model: LocationInformationModel!

    override func setUp() {
        // 가상의 네트워크 모델을 주고 시작한다.
        self.model = LocationInformationModel(localNetwork: stubNetwork)
        self.doc = cvsList
    }

    
    func testdocumentsToCellData() {
        let cellData = model.documentsToCellData(doc) // 실제 모델의 값
        let placeName = doc.map { $0.placeName } // dummy의 값
        let address0 = cellData[1].address// 실제 모델의 값
        let roadAddressName = doc[1].roadAddressName // dummy의 값
        
        // 실제와 동일해야 한다. 즉, 외부의 값 자체가 달라져서는 안된다.
        expect(cellData.map {$0.placeName}).to(
            equal(placeName),
            description: "DetailListCellData의 placeName은 document의 PlaceName이다")
        
        expect(address0).to(
            equal(roadAddressName),
            description: "KLDocument의 RoadAddressName이 빈 값이 아닐경우 roadAddress가 CellData에 전달된다.")
    }

}

Nimble을 사용했는데, XCTAssertEqual과 동일한 메소드이다. 

 

 

🟠 LocalNetworkStub.swift

//
//  LocalNetworkStub.swift
//  FindCVSTests
//
//  Created by Hamlit Jason on 2022/02/20.
//

import Foundation
import Stubber // 네트워크 상에서 너무 변수가 많으니까 가상의 네트워크에서 주입하는거
import RxSwift

@testable import FindCVS

class LocalNetworkStub: LocalNetwork {
    // LocalNetwork 상속 받아서 사용함.
    override func getLocation(by mapPoint: MTMapPoint) -> Single<Result<LocationData, URLError>> {
        return Stubber.invoke(getLocation, args: mapPoint)
    }
}

Stubber를 활용하여 네트워크 상에서 변수가 많으니까 네트워크를 주입하는 방법

Stubber에 대해서는 따로 포스팅을 해보자.

 

 

🟠 LocationInformationViewModelTests.swift

//
//  LocationInformationViewModelTests.swift
//  FindCVSTests
//
//  Created by Bo-Young PARK on 2021/09/26.
//

import XCTest
import Nimble
import RxSwift
import RxTest

@testable import FindCVS

class LocationInformationViewModelTests: XCTestCase {
    let disposeBag = DisposeBag()
    
    let stubNetwork  = LocalNetworkStub()
    var model: LocationInformationModel!
    var viewModel: LocationInformationViewModel!
    var doc: [KLDocument]!
    
    override func setUp() {
        self.model = LocationInformationModel(localNetwork: stubNetwork)
        self.viewModel = LocationInformationViewModel(model: model)
        self.doc = cvsList
    }
    
    func testSetMapCenter() {
        let scheduler = TestScheduler(initialClock: 0)
        
        //더미데이터 이벤트
        let dummyDataEvent = scheduler.createHotObservable([
            .next(0, cvsList)
        ])
        
        let documentData = PublishSubject<[KLDocument]>()
        dummyDataEvent
            .subscribe(documentData)
            .disposed(by: disposeBag)
        
        //DetailList 아이템(셀) 탭 이벤트
        let itemSelectedEvent = scheduler.createHotObservable([
            .next(1, 0)
        ])
        
        let itemSelected = PublishSubject<Int>()
        itemSelectedEvent
            .subscribe(itemSelected)
            .disposed(by: disposeBag)
        
        let selectedItemMapPoint = itemSelected
            .withLatestFrom(documentData) { $1[0] }
            .map(model.documentToMTMapPoint)
        
        //최초 현재 위치 이벤트
        let initialMapPoint = MTMapPoint(geoCoord: MTMapPointGeo(latitude: Double(37.394225), longitude: Double(127.110341)))!
        let currentLocationEvent = scheduler.createHotObservable([
            .next(0, initialMapPoint)
        ])
        
        let initialCurrentLocation = PublishSubject<MTMapPoint>()
        
        currentLocationEvent
            .subscribe(initialCurrentLocation)
            .disposed(by: disposeBag)
        
        //현재 위치 버튼 탭 이벤트
        let currentLocationButtonTapEvent = scheduler.createHotObservable([
            .next(2, Void()),
            .next(3, Void())
        ])
        
        let currentLocationButtonTapped = PublishSubject<Void>()
        
        currentLocationButtonTapEvent
            .subscribe(currentLocationButtonTapped)
            .disposed(by: disposeBag)
        
        let moveToCurrentLocation = currentLocationButtonTapped
            .withLatestFrom(initialCurrentLocation)
        
        // Merge
        let currentMapCenter = Observable
            .merge(
                selectedItemMapPoint,
                initialCurrentLocation.take(1),
                moveToCurrentLocation
            )
        
        let currentMapCenterObserver = scheduler.createObserver(Double.self)
        
        currentMapCenter
            .map { $0.mapPointGeo().latitude } // lat,lng 둘다 검사 아니라 lat만 검사
            .subscribe(currentMapCenterObserver)
            .disposed(by: disposeBag)
        
        scheduler.start()
        
        let secondMapPoint = model.documentToMTMapPoint(doc[0])
        
        expect(currentMapCenterObserver.events).to(
            equal([
                .next(0, initialMapPoint.mapPointGeo().latitude),
                .next(1, secondMapPoint.mapPointGeo().latitude),
                .next(2, initialMapPoint.mapPointGeo().latitude), // 내위치니까 결국 최초위치랑 같아야 한다.
                .next(3, initialMapPoint.mapPointGeo().latitude)
            ])
        )
    }
}

주석 읽어보면 쉽게 이해할 수 있으니까 천천히 읽어보기.

로직 자체가 어렵지는 않은데, rx를 정말 다채롭게 활용하는거 같아서 또 이렇게 배워감 ㅎㅎ

 

 

🟠 Dummy.swift

//
//  Dummy.swift
//  FindCVSTests
//
//  Created by Bo-Young PARK on 2021/09/26.
//

import Foundation

@testable import FindCVS

var cvsList: [KLDocument] = Dummy().load("networkDummy.json")

class Dummy {
    func load<T: Decodable>(_ filename: String) -> T {
        let data: Data
        let bundle = Bundle(for: type(of: self))
        
        guard let file = bundle.url(forResource: filename, withExtension: nil) else {
            fatalError("\(filename)을 main bundle에서 불러올 수 없습니다.")
        }
        
        do {
            data = try Data(contentsOf: file)
        } catch {
            fatalError("\(filename)을 main bundle에서 불러올 수 없습니다.\(error)")
        }
        
        do {
            let decoder = JSONDecoder()
            return try decoder.decode(T.self, from: data)
        } catch {
            fatalError("\(filename)을 \(T.self)로 파싱할 수 없습니다.")
        }
    }
}

cvsList를 전역으로 설정해 두어서 이것도 인상적이다.

load<T: Decodable> 에서 제네릭을 사용하는데, 이거 연습 더 해보자!

 

 

🟠 networkDummy.json

[
    {
        "address_name": "경기 성남시 분당구 백현동 537",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > 이마트24",
        "distance": "2",
        "id": "1306112074",
        "phone": "031-622-7176",
        "place_name": "이마트24 R판교알파돔2호점",
        "place_url": "http://place.map.kakao.com/1306112074",
        "road_address_name": "경기 성남시 분당구 판교역로 152",
        "x": "127.110346616745",
        "y": "37.394204373845"
    },
    {
        "address_name": "경기 성남시 분당구 백현동 132-189",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > GS25",
        "distance": "74",
        "id": "16486797",
        "phone": "031-707-6410",
        "place_name": "GS25 S판교역점",
        "place_url": "http://place.map.kakao.com/16486797",
        "road_address_name": "경기 성남시 분당구 판교역로 지하 160",
        "x": "127.11079919194681",
        "y": "37.39478420869054"
    },
    {
        "address_name": "경기 성남시 분당구 백현동 530",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > 세븐일레븐",
        "distance": "101",
        "id": "1003710555",
        "phone": "",
        "place_name": "세븐일레븐 판교알파본점",
        "place_url": "http://place.map.kakao.com/1003710555",
        "road_address_name": "경기 성남시 분당구 판교역로 145",
        "x": "127.109194827252",
        "y": "37.3942739207134"
    },
    {
        "address_name": "경기 성남시 분당구 백현동 535",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > 이마트24",
        "distance": "158",
        "id": "327879494",
        "phone": "",
        "place_name": "이마트24 R판교알파돔",
        "place_url": "http://place.map.kakao.com/327879494",
        "road_address_name": "경기 성남시 분당구 분당내곡로 117",
        "x": "127.112130909636",
        "y": "37.394199091245"
    },
    {
        "address_name": "경기 성남시 분당구 백현동 531",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > 세븐일레븐",
        "distance": "216",
        "id": "1824252087",
        "phone": "",
        "place_name": "세븐일레븐 판교알파점",
        "place_url": "http://place.map.kakao.com/1824252087",
        "road_address_name": "경기 성남시 분당구 대왕판교로606번길 10",
        "x": "127.10903452806154",
        "y": "37.395876085264874"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 646",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점",
        "distance": "253",
        "id": "18830408",
        "phone": "031-8017-2851",
        "place_name": "원스토어편의점",
        "place_url": "http://place.map.kakao.com/18830408",
        "road_address_name": "경기 성남시 분당구 판교역로 178",
        "x": "127.110042792158",
        "y": "37.396493252041"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 651",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점",
        "distance": "266",
        "id": "21610813",
        "phone": "031-8017-0662",
        "place_name": "원타임 해피판교역점",
        "place_url": "http://place.map.kakao.com/21610813",
        "road_address_name": "경기 성남시 분당구 대왕판교로606번길 39",
        "x": "127.11150407155293",
        "y": "37.396436019780545"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 647",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > CU",
        "distance": "274",
        "id": "19729729",
        "phone": "031-8017-6771",
        "place_name": "CU 판교역점",
        "place_url": "http://place.map.kakao.com/19729729",
        "road_address_name": "경기 성남시 분당구 대왕판교로606번길 31",
        "x": "127.11096907172671",
        "y": "37.396643758006725"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 741",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > CU",
        "distance": "274",
        "id": "26776001",
        "phone": "031-702-0135",
        "place_name": "CU 판교월드마크점",
        "place_url": "http://place.map.kakao.com/26776001",
        "road_address_name": "경기 성남시 분당구 대왕판교로606번길 58",
        "x": "127.11329416621403",
        "y": "37.394998089782916"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 643",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > 이마트24",
        "distance": "327",
        "id": "1970035485",
        "phone": "070-8801-5864",
        "place_name": "이마트24 판교역점",
        "place_url": "http://place.map.kakao.com/1970035485",
        "road_address_name": "경기 성남시 분당구 판교역로 184",
        "x": "127.11013412793073",
        "y": "37.39716893309608"
    },
    {
        "address_name": "경기 성남시 분당구 백현동 529",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > GS25",
        "distance": "358",
        "id": "27498673",
        "phone": "",
        "place_name": "GS25 판교허브점",
        "place_url": "http://place.map.kakao.com/27498673",
        "road_address_name": "경기 성남시 분당구 판교역로 109",
        "x": "127.109027707855",
        "y": "37.3911673507558"
    },
    {
        "address_name": "경기 성남시 분당구 백현동 529",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > CU",
        "distance": "358",
        "id": "26776000",
        "phone": "031-708-4755",
        "place_name": "CU 판교SK허브점",
        "place_url": "http://place.map.kakao.com/26776000",
        "road_address_name": "경기 성남시 분당구 판교역로 109",
        "x": "127.109027707855",
        "y": "37.3911673507558"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 661",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > GS25",
        "distance": "360",
        "id": "2137856685",
        "phone": "1644-5425",
        "place_name": "GS25 판교예미지점",
        "place_url": "http://place.map.kakao.com/2137856685",
        "road_address_name": "경기 성남시 분당구 판교역로192번길 14-1",
        "x": "127.11173796948",
        "y": "37.3972809570947"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 655",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > 이마트24",
        "distance": "361",
        "id": "2141413165",
        "phone": "031-708-8334",
        "place_name": "이마트24 판교SG점",
        "place_url": "http://place.map.kakao.com/2141413165",
        "road_address_name": "경기 성남시 분당구 대왕판교로606번길 47",
        "x": "127.1123881246",
        "y": "37.3970460753739"
    },
    {
        "address_name": "경기 성남시 분당구 삼평동 662",
        "category_group_code": "CS2",
        "category_group_name": "편의점",
        "category_name": "가정,생활 > 편의점 > GS25",
        "distance": "407",
        "id": "979082513",
        "phone": "031-781-6910",
        "place_name": "GS25 판교타워점",
        "place_url": "http://place.map.kakao.com/979082513",
        "road_address_name": "경기 성남시 분당구 판교역로192번길 16",
        "x": "127.11141235322867",
        "y": "37.39779845058796"
    }
]

JSON 예시 데이터