From 0c4b14debf2d611e2a03e9d0d0da12b03bb55084 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Sat, 22 Feb 2025 22:37:14 +0300 Subject: [PATCH] Adding tests for search screen --- .../xcshareddata/xcschemes/AutoCat.xcscheme | 3 +- .../SearchScreen/SearchCoordinator.swift | 1 - .../SearchScreen/SearchViewModel.swift | 24 +- AutoCatCore/Models/PagedResponse.swift | 14 +- AutoCatTests/HistoryTests.swift | 2 +- AutoCatTests/SearchTests.swift | 241 ++++++++++++++++++ 6 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 AutoCatTests/SearchTests.swift diff --git a/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme b/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme index 579b984..cc06184 100644 --- a/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme +++ b/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> diff --git a/AutoCat/Screens/SearchScreen/SearchCoordinator.swift b/AutoCat/Screens/SearchScreen/SearchCoordinator.swift index 1ae01e7..3fbcddc 100644 --- a/AutoCat/Screens/SearchScreen/SearchCoordinator.swift +++ b/AutoCat/Screens/SearchScreen/SearchCoordinator.swift @@ -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 diff --git a/AutoCat/Screens/SearchScreen/SearchViewModel.swift b/AutoCat/Screens/SearchScreen/SearchViewModel.swift index af45de4..01a3d9b 100644 --- a/AutoCat/Screens/SearchScreen/SearchViewModel.swift +++ b/AutoCat/Screens/SearchScreen/SearchViewModel.swift @@ -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)) - await reloadData(with: filter) + 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) } } } diff --git a/AutoCatCore/Models/PagedResponse.swift b/AutoCatCore/Models/PagedResponse.swift index e3df2aa..6f42cfe 100644 --- a/AutoCatCore/Models/PagedResponse.swift +++ b/AutoCatCore/Models/PagedResponse.swift @@ -1,13 +1,13 @@ import Foundation -public final class PagedResponse: Decodable, Sendable where T: Decodable & Sendable { - public let count: Int? - public let pageToken: String? - public let items: [T] +public struct PagedResponse: 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 } } diff --git a/AutoCatTests/HistoryTests.swift b/AutoCatTests/HistoryTests.swift index 1a525fd..51126ac 100644 --- a/AutoCatTests/HistoryTests.swift +++ b/AutoCatTests/HistoryTests.swift @@ -21,7 +21,7 @@ struct HistoryTests { var viewModel: HistoryViewModel - init() async { + init() { viewModel = HistoryViewModel( apiService: apiServiceMock, diff --git a/AutoCatTests/SearchTests.swift b/AutoCatTests/SearchTests.swift new file mode 100644 index 0000000..6ea6811 --- /dev/null +++ b/AutoCatTests/SearchTests.swift @@ -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)) + } +}