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"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ struct HistoryTests {
var viewModel: HistoryViewModel
init() async {
init() {
viewModel = HistoryViewModel(
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))
}
}