Skip to content

Commit

Permalink
Merge pull request #339 from boostcampwm-2021/test/SearchViewModelTests
Browse files Browse the repository at this point in the history
[테스트] SearchViewModel 테스트
  • Loading branch information
Modyhoon authored Dec 2, 2021
2 parents bab9d43 + cb231dc commit 3eb9bf4
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 17 deletions.
32 changes: 30 additions & 2 deletions BBus/BBus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@
04468439275677C0007E440A /* MovingStatusAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468438275677C0007E440A /* MovingStatusAPIUsable.swift */; };
0446843B275677CA007E440A /* MovingStatusCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843A275677CA007E440A /* MovingStatusCalculatable.swift */; };
0446843D275679B9007E440A /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; };
0446843E27567F4E007E440A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51F2272FCDF900EC0531 /* SearchViewModel.swift */; };
0446843F27568065007E440A /* SearchAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468426275675F1007E440A /* SearchAPIUsable.swift */; };
0446844027568075007E440A /* SearchCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044684282756760A007E440A /* SearchCalculatable.swift */; };
0446844127568084007E440A /* BusRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AA273B9F330061ACDA /* BusRouteDTO.swift */; };
0446844227568091007E440A /* StationDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AE273BAA130061ACDA /* StationDTO.swift */; };
0446844327568096007E440A /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04045B7E2740F6B20056A433 /* SearchResults.swift */; };
04468444275680A0007E440A /* BusSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04045B802740F6DC0056A433 /* BusSearchResult.swift */; };
04468445275680B1007E440A /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; };
04468446275680FF007E440A /* StationSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51EE272FCDE600EC0531 /* StationSearchResult.swift */; };
0446844727568119007E440A /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; };
044684482756812D007E440A /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; };
0446844927568134007E440A /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; };
0446844A27568A3B007E440A /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5A10273D2E1B00490075 /* StringExtension.swift */; };
0446844D275694D7007E440A /* SearchCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0451EDB32755BBBD00031A16 /* SearchCalculateUseCase.swift */; };
0451EDB42755BBBD00031A16 /* SearchCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0451EDB32755BBBD00031A16 /* SearchCalculateUseCase.swift */; };
0476ABBB27310ED200F72DD1 /* BusRouteHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0476ABBA27310ED200F72DD1 /* BusRouteHeaderView.swift */; };
0476ABBD27311C1600F72DD1 /* BusStationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0476ABBC27311C1600F72DD1 /* BusStationTableViewCell.swift */; };
Expand Down Expand Up @@ -1806,7 +1820,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0446844727568119007E440A /* PublisherExtension.swift in Sources */,
044684482756812D007E440A /* JsonDTO.swift in Sources */,
0446843E27567F4E007E440A /* SearchViewModel.swift in Sources */,
4AF1E0A92756268B00DE51C8 /* SearchViewModelTests.swift in Sources */,
0446844D275694D7007E440A /* SearchCalculateUseCase.swift in Sources */,
04468445275680B1007E440A /* BaseUseCase.swift in Sources */,
0446844127568084007E440A /* BusRouteDTO.swift in Sources */,
0446844927568134007E440A /* BBusAPIError.swift in Sources */,
0446844027568075007E440A /* SearchCalculatable.swift in Sources */,
0446844A27568A3B007E440A /* StringExtension.swift in Sources */,
0446844327568096007E440A /* SearchResults.swift in Sources */,
0446844227568091007E440A /* StationDTO.swift in Sources */,
0446843F27568065007E440A /* SearchAPIUsable.swift in Sources */,
04468444275680A0007E440A /* BusSearchResult.swift in Sources */,
04468446275680FF007E440A /* StationSearchResult.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -2439,7 +2467,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = B3PWYBKFUK;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -2463,7 +2491,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = B3PWYBKFUK;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
8 changes: 4 additions & 4 deletions BBus/BBus/Foreground/Search/ViewModel/SearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class SearchViewModel {
@Published private(set) var networkError: Error?
private var cancellables: Set<AnyCancellable>

init(apiUseCase: SearchAPIUseCase, calculateUseCase: SearchCalculatable) {
init(apiUseCase: SearchAPIUsable, calculateUseCase: SearchCalculatable) {
self.apiUseCase = apiUseCase
self.calculateUseCase = calculateUseCase
self.keyword = ""
Expand Down Expand Up @@ -69,10 +69,10 @@ final class SearchViewModel {
.sink { [weak self] keyword in
guard let self = self else { return }

self.searchResults.busSearchResults = self.calculateUseCase.searchBus(by: keyword, at: self.busRouteList)
self.searchResults.stationSearchResults = self.calculateUseCase.searchStation(by: keyword, at: self.stationList)
let busSearchResults = self.calculateUseCase.searchBus(by: keyword, at: self.busRouteList)
let stationSearchResults = self.calculateUseCase.searchStation(by: keyword, at: self.stationList)
self.searchResults = SearchResults(busSearchResults: busSearchResults, stationSearchResults: stationSearchResults)
}
.store(in: &self.cancellables)
}

}
201 changes: 190 additions & 11 deletions BBus/SearchViewModelTests/SearchViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,206 @@
//

import XCTest
import Combine

class SearchViewModelTests: XCTestCase {

enum MOCKMode {
case success, loadBusRouteFailure, loadStationFailure, alwaysFailure
}

enum TestError: Error {
case loadBusRouteError, loadStationListError
}

class MOCKSearchAPIUseCase: SearchAPIUsable {
var mode: MOCKMode

init(mode: MOCKMode) {
self.mode = mode
}

func loadBusRouteList() -> AnyPublisher<[BusRouteDTO], Error> {
switch mode {
case .loadBusRouteFailure, .alwaysFailure:
return Fail(error: TestError.loadBusRouteError).eraseToAnyPublisher()
default:
let busRouteDTOs = [ BusRouteDTO(routeID: 1, busRouteName: "1", routeType: .mainLine, startStation: "1s", endStation: "1e"),
BusRouteDTO(routeID: 2, busRouteName: "2", routeType: .mainLine, startStation: "2s", endStation: "2e"),
BusRouteDTO(routeID: 3, busRouteName: "3", routeType: .mainLine, startStation: "3s", endStation: "3e")]
return Just(busRouteDTOs)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}

func loadStationList() -> AnyPublisher<[StationDTO], Error> {
switch mode {
case .loadStationFailure, .alwaysFailure:
return Fail(error: TestError.loadStationListError).eraseToAnyPublisher()
default:
let stationDTOs = [StationDTO(stationID: 1, arsID: "1", stationName: "station1"),
StationDTO(stationID: 2, arsID: "2", stationName: "station2"),
StationDTO(stationID: 3, arsID: "3", stationName: "station3"),
StationDTO(stationID: 4, arsID: "4", stationName: "station4")]
return Just(stationDTOs)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
}

private var cancellables: Set<AnyCancellable>!
private let firstBusRoute = BusSearchResult(busRouteDTO: BusRouteDTO(routeID: 1,
busRouteName: "1",
routeType: .mainLine,
startStation: "1s",
endStation: "1e"))
private let firstStation = StationSearchResult(stationName: "station1",
arsId: "1",
stationNameMatchRanges: "station1".ranges(of: "1"),
arsIdMatchRanges: "1".ranges(of: "1"))

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
self.cancellables = []
super.setUp()
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
self.cancellables = nil
super.tearDown()
}

func test_bindSearchResult_성공() {
let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .success),
calculateUseCase: SearchCalculateUseCase())
let keyword = "1"
let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인")
let expectedResult = SearchResults(busSearchResults: [self.firstBusRoute], stationSearchResults: [self.firstStation])
let firstExpectedBus = expectedResult.busSearchResults[0]
let firstExpectedStation = expectedResult.stationSearchResults[0]

// when then
searchViewModel.$searchResults
.receive(on: DispatchQueue.global())
.dropFirst()
.sink { searchResults in
let firstBus = searchResults.busSearchResults[0]
let firstStation = searchResults.stationSearchResults[0]

XCTAssertEqual(searchResults.busSearchResults.count, expectedResult.busSearchResults.count)
XCTAssertEqual(firstBus.busRouteName, firstExpectedBus.busRouteName)
XCTAssertEqual(firstBus.routeID, firstExpectedBus.routeID)
XCTAssertEqual(firstBus.routeType, firstExpectedBus.routeType)
XCTAssertEqual(searchResults.stationSearchResults.count, expectedResult.stationSearchResults.count)
XCTAssertEqual(firstStation.arsId, firstExpectedStation.arsId)
XCTAssertEqual(firstStation.arsIdMatchRanges, firstExpectedStation.arsIdMatchRanges)
XCTAssertEqual(firstStation.stationName, firstExpectedStation.stationName)
XCTAssertEqual(firstStation.stationNameMatchRanges, firstExpectedStation.stationNameMatchRanges)

expectation.fulfill()
}
.store(in: &self.cancellables)
searchViewModel.configure(keyword: keyword)

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.
waitForExpectations(timeout: 10)
}

func test_bindSearchResult_버스와_정류소목록_둘_다_없어서_실패() {
let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .alwaysFailure),
calculateUseCase: SearchCalculateUseCase())
let keyword = "1"
let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인")

// when then
searchViewModel.$searchResults
.receive(on: DispatchQueue.global())
.dropFirst()
.sink { searchResults in
XCTAssertTrue(searchResults.busSearchResults.isEmpty)
XCTAssertTrue(searchResults.busSearchResults.isEmpty)

expectation.fulfill()
}
.store(in: &self.cancellables)
searchViewModel.configure(keyword: keyword)

func testPerformanceExample() throws {
// This is an example of a performance test case.
measure {
// Put the code you want to measure the time of here.
}
waitForExpectations(timeout: 10)
}

func test_bindSearchResult_버스_목록이_없어서_실패() {
let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .loadBusRouteFailure),
calculateUseCase: SearchCalculateUseCase())
let keyword = "1"
let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인")
let expectedResult = SearchResults(busSearchResults: [], stationSearchResults: [self.firstStation])
let firstExpectedStation = expectedResult.stationSearchResults[0]
let expectedError = TestError.loadBusRouteError

// when then
searchViewModel.$searchResults
.receive(on: DispatchQueue.global())
.dropFirst()
.sink { searchResults in
let firstStation = searchResults.stationSearchResults[0]

XCTAssertTrue(searchResults.busSearchResults.isEmpty)
XCTAssertEqual(searchResults.stationSearchResults.count, expectedResult.stationSearchResults.count)
XCTAssertEqual(firstStation.arsId, firstExpectedStation.arsId)
XCTAssertEqual(firstStation.arsIdMatchRanges, firstExpectedStation.arsIdMatchRanges)
XCTAssertEqual(firstStation.stationName, firstExpectedStation.stationName)
XCTAssertEqual(firstStation.stationNameMatchRanges, firstExpectedStation.stationNameMatchRanges)

guard let error = searchViewModel.networkError as? TestError else { XCTFail(); return }
switch error {
case expectedError:
break
default:
XCTFail()
}

expectation.fulfill()
}
.store(in: &self.cancellables)
searchViewModel.configure(keyword: keyword)

waitForExpectations(timeout: 10)
}

func test_bindSearchResult_정류소_목록이_없어서_실패() {
let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .loadStationFailure),
calculateUseCase: SearchCalculateUseCase())
let keyword = "1"
let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인")
let expectedResult = SearchResults(busSearchResults:[self.firstBusRoute], stationSearchResults: [])
let firstExpectedBus = expectedResult.busSearchResults[0]
let expectedError = TestError.loadStationListError

// when then
searchViewModel.$searchResults
.receive(on: DispatchQueue.global())
.dropFirst()
.sink { searchResults in
let firstBus = searchResults.busSearchResults[0]

XCTAssertEqual(searchResults.busSearchResults.count, expectedResult.busSearchResults.count)
XCTAssertEqual(firstBus.busRouteName, firstExpectedBus.busRouteName)
XCTAssertEqual(firstBus.routeID, firstExpectedBus.routeID)
XCTAssertEqual(firstBus.routeType, firstExpectedBus.routeType)
XCTAssertTrue(searchResults.stationSearchResults.isEmpty)

guard let error = searchViewModel.networkError as? TestError else { XCTFail(); return }
switch error {
case expectedError:
break
default:
XCTFail()
}

expectation.fulfill()
}
.store(in: &self.cancellables)
searchViewModel.configure(keyword: keyword)

waitForExpectations(timeout: 10)
}
}

0 comments on commit 3eb9bf4

Please sign in to comment.