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 예시 데이터
'Archive > 패캠(초격차)' 카테고리의 다른 글
part4 (ch1). MyAssets 코드리뷰(feat. SwiftUI) (0) | 2022.02.23 |
---|---|
part5 (ch1). FindCVS 코드리뷰 (0) | 2022.02.20 |
part5 (ch6). KeywordNews XCTest 코드리뷰 (0) | 2022.02.17 |
part5 (ch6). KeywordNews 코드리뷰 (0) | 2022.02.15 |
part5 (ch6). 🪛 CI/CD란? (feat. bitrise) (0) | 2022.02.15 |