Adding tests for search screen

This commit is contained in:
Selim Mustafaev 2025-02-22 22:37:14 +03:00
parent 071a6f800d
commit 0c4b14debf
6 changed files with 264 additions and 21 deletions

View File

@ -26,7 +26,8 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables> <Testables>
<TestableReference <TestableReference
skipped = "NO"> skipped = "NO">

View File

@ -20,7 +20,6 @@ final class SearchCoordinator {
let resolver = ServiceContainer.shared let resolver = ServiceContainer.shared
let viewModel = SearchViewModel( let viewModel = SearchViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self), apiService: resolver.resolve(ApiServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self),
vehicleService: resolver.resolve(VehicleServiceProtocol.self) vehicleService: resolver.resolve(VehicleServiceProtocol.self)
) )
viewModel.coordinator = self viewModel.coordinator = self

View File

@ -15,7 +15,6 @@ import Combine
final class SearchViewModel: ACHudContainer { final class SearchViewModel: ACHudContainer {
let apiService: ApiServiceProtocol let apiService: ApiServiceProtocol
let storageService: StorageServiceProtocol
let vehicleService: VehicleServiceProtocol let vehicleService: VehicleServiceProtocol
var coordinator: SearchCoordinator? var coordinator: SearchCoordinator?
@ -35,8 +34,10 @@ final class SearchViewModel: ACHudContainer {
if searchText != oldValue { if searchText != oldValue {
filter.searchString = searchText filter.searchString = searchText
searchTask = Task { [filter] in searchTask = Task { [filter] in
try? await Task.sleep(for: .milliseconds(500)) do {
await reloadData(with: filter) try await Task.sleep(for: .milliseconds(500))
await reloadData(with: filter)
} catch {}
} }
} }
} }
@ -51,11 +52,9 @@ final class SearchViewModel: ACHudContainer {
} }
init(apiService: ApiServiceProtocol, init(apiService: ApiServiceProtocol,
storageService: StorageServiceProtocol,
vehicleService: VehicleServiceProtocol) { vehicleService: VehicleServiceProtocol) {
self.apiService = apiService self.apiService = apiService
self.storageService = storageService
self.vehicleService = vehicleService self.vehicleService = vehicleService
} }
@ -80,7 +79,6 @@ final class SearchViewModel: ACHudContainer {
func loadSearchResults(filter: Filter) async throws { func loadSearchResults(filter: Filter) async throws {
let query = filter.searchString
let response = try await apiService.getVehicles(with: filter, pageToken: pageToken, pageSize: 20) let response = try await apiService.getVehicles(with: filter, pageToken: pageToken, pageSize: 20)
if response.items.isEmpty { if response.items.isEmpty {
@ -172,18 +170,22 @@ final class SearchViewModel: ACHudContainer {
} }
func updateVehicle(_ vehicle: VehicleDto) async { func updateVehicle(_ vehicle: VehicleDto) async {
await wrapWithToast { [weak self] in do {
guard let self else { hud = .progress
let (updatedVehicle, errors) = try await vehicleService.updateSearch(number: vehicle.number)
if !errors.isEmpty {
showErrors(errors)
return return
} }
let updatedVehicle = try await apiService.checkVehicle(by: vehicle.getNumber(), notes: [], events: [], force: true)
try await storageService.updateVehicle(dto: updatedVehicle, policy: .ifExists)
if let index = vehicles.firstIndex(where: { $0.number == updatedVehicle.number }) { if let index = vehicles.firstIndex(where: { $0.number == updatedVehicle.number }) {
vehicles[index] = updatedVehicle vehicles[index] = updatedVehicle
vehicleSections = vehicles.groupedByDate(type: .updatedDate) vehicleSections = vehicles.groupedByDate(type: .updatedDate)
} }
hud = nil
} catch {
hud = .error(error)
} }
} }
} }

View File

@ -1,13 +1,13 @@
import Foundation import Foundation
public final class PagedResponse<T>: Decodable, Sendable where T: Decodable & Sendable { public struct PagedResponse<T>: Decodable, Sendable where T: Decodable & Sendable {
public let count: Int? public var count: Int?
public let pageToken: String? public var pageToken: String?
public let items: [T] public var items: [T]
public init() { public init(items: [T]) {
self.items = [] self.count = items.count
self.count = nil
self.pageToken = nil self.pageToken = nil
self.items = items
} }
} }

View File

@ -21,7 +21,7 @@ struct HistoryTests {
var viewModel: HistoryViewModel var viewModel: HistoryViewModel
init() async { init() {
viewModel = HistoryViewModel( viewModel = HistoryViewModel(
apiService: apiServiceMock, apiService: apiServiceMock,

View File

@ -0,0 +1,241 @@
//
// SearchTests.swift
// AutoCatTests
//
// Created by Selim Mustafaev on 22.02.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Testing
import Mockable
import AutoCatCore
@testable import AutoCat
@MainActor
struct SearchTests {
let apiServiceMock = MockApiServiceProtocol()
let vehicleServiceMock = MockVehicleServiceProtocol()
var viewModel: SearchViewModel
init() {
viewModel = SearchViewModel(
apiService: apiServiceMock,
vehicleService: vehicleServiceMock
)
}
@Test("Initial load")
func initialLoad() async {
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .value(nil), pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
await viewModel.onAppear()
verify(apiServiceMock)
.getVehicles(with: .any, pageToken: .value(nil), pageSize: .any)
.called(.once)
#expect(viewModel.vehicles == [.normal])
#expect(viewModel.vehicleSections.count == 1)
#expect(viewModel.searchText == "")
#expect(viewModel.hud == nil)
}
@Test("Initial load error")
func initialLoadError() async {
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .value(nil), pageSize: .any)
.willThrow(TestError.generic)
await viewModel.onAppear()
#expect(viewModel.vehicles.isEmpty)
#expect(viewModel.hud == .error(TestError.generic))
}
@Test("Search")
func search() async throws {
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
await viewModel.onAppear()
viewModel.searchText = "test1"
viewModel.searchText = "test2"
try await Task.sleep(for: .milliseconds(600))
viewModel.searchText = "test3"
try await Task.sleep(for: .milliseconds(600))
verify(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.called(.exactly(3))
#expect(viewModel.vehicles == [.normal])
#expect(viewModel.vehicleSections.count == 1)
#expect(viewModel.filter.searchString == "test3")
#expect(viewModel.hud == nil)
}
@Test("Search error")
func searchError() async throws {
var filterWithString = Filter()
filterWithString.searchString = "test"
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willThrow(TestError.generic)
await viewModel.onAppear()
viewModel.searchText = "test"
try await Task.sleep(for: .milliseconds(600))
#expect(viewModel.vehicles.isEmpty)
#expect(viewModel.hud == .error(TestError.generic))
}
@Test("Pagination")
func pagination() async {
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: []))
await viewModel.onAppear()
await viewModel.loadMoreData()
await viewModel.loadMoreData()
verify(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.called(.exactly(3))
#expect(viewModel.vehicles.count == 2)
#expect(viewModel.hasMoreData == false)
#expect(viewModel.hud == nil)
}
@Test("Pagination error")
func paginationError() async {
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willThrow(TestError.generic)
await viewModel.onAppear()
await viewModel.loadMoreData()
await viewModel.loadMoreData()
verify(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.called(.exactly(3))
#expect(viewModel.vehicles.count == 2)
#expect(viewModel.hasMoreData == false)
#expect(viewModel.hud == .error(TestError.generic))
}
@Test("Update vehicle")
func updateVehicle() async {
let updatedVehicle: VehicleDto = .normal
.addEvent(.valid)
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
given(vehicleServiceMock)
.updateSearch(number: .value(VehicleDto.normal.number))
.willReturn((updatedVehicle, []))
await viewModel.onAppear()
await viewModel.updateVehicle(.normal)
verify(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.called(.once)
verify(vehicleServiceMock)
.updateSearch(number: .any)
.called(.once)
#expect(viewModel.vehicles.first?.events.isEmpty == false)
#expect(viewModel.hud == nil)
}
@Test("Update vehicle error")
func updateVehicleError() async {
let updatedVehicle: VehicleDto = .normal
.addEvent(.valid)
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
given(vehicleServiceMock)
.updateSearch(number: .value(VehicleDto.normal.number))
.willReturn((updatedVehicle, [TestError.generic]))
await viewModel.onAppear()
await viewModel.updateVehicle(.normal)
verify(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.called(.once)
verify(vehicleServiceMock)
.updateSearch(number: .any)
.called(.once)
#expect(viewModel.vehicles.first?.events.isEmpty == true)
if case .message(_, _) = viewModel.hud {} else {
Issue.record("hud must be in .message state")
}
}
@Test("Update vehicle error 2")
func updateVehicleError2() async {
given(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.willReturn(PagedResponse(items: [VehicleDto.normal]))
given(vehicleServiceMock)
.updateSearch(number: .value(VehicleDto.normal.number))
.willThrow(TestError.generic)
await viewModel.onAppear()
await viewModel.updateVehicle(.normal)
verify(apiServiceMock)
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
.called(.once)
verify(vehicleServiceMock)
.updateSearch(number: .any)
.called(.once)
#expect(viewModel.vehicles.first?.events.isEmpty == true)
#expect(viewModel.hud == .error(TestError.generic))
}
}