Files
PlantGuide/PlantGuideTests/Mocks/MockPlantCollectionRepository.swift
Trey t 136dfbae33 Add PlantGuide iOS app with plant identification and care management
- Implement camera capture and plant identification workflow
- Add Core Data persistence for plants, care schedules, and cached API data
- Create collection view with grid/list layouts and filtering
- Build plant detail views with care information display
- Integrate Trefle botanical API for plant care data
- Add local image storage for captured plant photos
- Implement dependency injection container for testability
- Include accessibility support throughout the app

Bug fixes in this commit:
- Fix Trefle API decoding by removing duplicate CodingKeys
- Fix LocalCachedImage to load from correct PlantImages directory
- Set dateAdded when saving plants for proper collection sorting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:18:01 -06:00

276 lines
7.8 KiB
Swift

//
// MockPlantCollectionRepository.swift
// PlantGuideTests
//
// Mock implementation of PlantCollectionRepositoryProtocol for unit testing.
// Provides configurable behavior and call tracking for verification.
//
import Foundation
@testable import PlantGuide
// MARK: - MockPlantCollectionRepository
/// Mock implementation of PlantCollectionRepositoryProtocol for testing
final class MockPlantCollectionRepository: PlantCollectionRepositoryProtocol, @unchecked Sendable {
// MARK: - Storage
var plants: [UUID: Plant] = [:]
// MARK: - Call Tracking
var saveCallCount = 0
var fetchByIdCallCount = 0
var fetchAllCallCount = 0
var deleteCallCount = 0
var existsCallCount = 0
var updatePlantCallCount = 0
var searchCallCount = 0
var filterCallCount = 0
var getFavoritesCallCount = 0
var setFavoriteCallCount = 0
var getStatisticsCallCount = 0
// MARK: - Error Configuration
var shouldThrowOnSave = false
var shouldThrowOnFetch = false
var shouldThrowOnDelete = false
var shouldThrowOnExists = false
var shouldThrowOnUpdate = false
var shouldThrowOnSearch = false
var shouldThrowOnFilter = false
var shouldThrowOnGetFavorites = false
var shouldThrowOnSetFavorite = false
var shouldThrowOnGetStatistics = false
var errorToThrow: Error = NSError(
domain: "MockError",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "Mock repository error"]
)
// MARK: - Captured Values
var lastSavedPlant: Plant?
var lastDeletedPlantID: UUID?
var lastUpdatedPlant: Plant?
var lastSearchQuery: String?
var lastFilter: PlantFilter?
var lastSetFavoritePlantID: UUID?
var lastSetFavoriteValue: Bool?
// MARK: - Statistics Configuration
var statisticsToReturn: CollectionStatistics?
// MARK: - PlantRepositoryProtocol
func save(_ plant: Plant) async throws {
saveCallCount += 1
lastSavedPlant = plant
if shouldThrowOnSave {
throw errorToThrow
}
plants[plant.id] = plant
}
func fetch(id: UUID) async throws -> Plant? {
fetchByIdCallCount += 1
if shouldThrowOnFetch {
throw errorToThrow
}
return plants[id]
}
func fetchAll() async throws -> [Plant] {
fetchAllCallCount += 1
if shouldThrowOnFetch {
throw errorToThrow
}
return Array(plants.values)
}
func delete(id: UUID) async throws {
deleteCallCount += 1
lastDeletedPlantID = id
if shouldThrowOnDelete {
throw errorToThrow
}
plants.removeValue(forKey: id)
}
// MARK: - PlantCollectionRepositoryProtocol Extensions
func exists(id: UUID) async throws -> Bool {
existsCallCount += 1
if shouldThrowOnExists {
throw errorToThrow
}
return plants[id] != nil
}
func updatePlant(_ plant: Plant) async throws {
updatePlantCallCount += 1
lastUpdatedPlant = plant
if shouldThrowOnUpdate {
throw errorToThrow
}
plants[plant.id] = plant
}
func searchPlants(query: String) async throws -> [Plant] {
searchCallCount += 1
lastSearchQuery = query
if shouldThrowOnSearch {
throw errorToThrow
}
let lowercaseQuery = query.lowercased()
return plants.values.filter { plant in
plant.scientificName.lowercased().contains(lowercaseQuery) ||
plant.commonNames.contains { $0.lowercased().contains(lowercaseQuery) } ||
(plant.notes?.lowercased().contains(lowercaseQuery) ?? false)
}
}
func filterPlants(by filter: PlantFilter) async throws -> [Plant] {
filterCallCount += 1
lastFilter = filter
if shouldThrowOnFilter {
throw errorToThrow
}
var result = Array(plants.values)
// Apply search query
if let query = filter.searchQuery, !query.isEmpty {
let lowercaseQuery = query.lowercased()
result = result.filter { plant in
plant.scientificName.lowercased().contains(lowercaseQuery) ||
plant.commonNames.contains { $0.lowercased().contains(lowercaseQuery) }
}
}
// Filter by favorites
if let isFavorite = filter.isFavorite {
result = result.filter { $0.isFavorite == isFavorite }
}
// Filter by families
if let families = filter.families, !families.isEmpty {
result = result.filter { families.contains($0.family) }
}
// Filter by identification source
if let source = filter.identificationSource {
result = result.filter { $0.identificationSource == source }
}
return result
}
func getFavorites() async throws -> [Plant] {
getFavoritesCallCount += 1
if shouldThrowOnGetFavorites {
throw errorToThrow
}
return plants.values.filter { $0.isFavorite }
.sorted { $0.dateIdentified > $1.dateIdentified }
}
func setFavorite(plantID: UUID, isFavorite: Bool) async throws {
setFavoriteCallCount += 1
lastSetFavoritePlantID = plantID
lastSetFavoriteValue = isFavorite
if shouldThrowOnSetFavorite {
throw errorToThrow
}
if var plant = plants[plantID] {
plant.isFavorite = isFavorite
plants[plantID] = plant
}
}
func getCollectionStatistics() async throws -> CollectionStatistics {
getStatisticsCallCount += 1
if shouldThrowOnGetStatistics {
throw errorToThrow
}
if let statistics = statisticsToReturn {
return statistics
}
// Calculate statistics from current plants
var familyDistribution: [String: Int] = [:]
var sourceBreakdown: [IdentificationSource: Int] = [:]
for plant in plants.values {
familyDistribution[plant.family, default: 0] += 1
sourceBreakdown[plant.identificationSource, default: 0] += 1
}
return CollectionStatistics(
totalPlants: plants.count,
favoriteCount: plants.values.filter { $0.isFavorite }.count,
familyDistribution: familyDistribution,
identificationSourceBreakdown: sourceBreakdown,
plantsAddedThisMonth: 0,
upcomingTasksCount: 0,
overdueTasksCount: 0
)
}
// MARK: - Helper Methods
/// Resets all state for clean test setup
func reset() {
plants = [:]
saveCallCount = 0
fetchByIdCallCount = 0
fetchAllCallCount = 0
deleteCallCount = 0
existsCallCount = 0
updatePlantCallCount = 0
searchCallCount = 0
filterCallCount = 0
getFavoritesCallCount = 0
setFavoriteCallCount = 0
getStatisticsCallCount = 0
shouldThrowOnSave = false
shouldThrowOnFetch = false
shouldThrowOnDelete = false
shouldThrowOnExists = false
shouldThrowOnUpdate = false
shouldThrowOnSearch = false
shouldThrowOnFilter = false
shouldThrowOnGetFavorites = false
shouldThrowOnSetFavorite = false
shouldThrowOnGetStatistics = false
lastSavedPlant = nil
lastDeletedPlantID = nil
lastUpdatedPlant = nil
lastSearchQuery = nil
lastFilter = nil
lastSetFavoritePlantID = nil
lastSetFavoriteValue = nil
statisticsToReturn = nil
}
/// Adds a plant directly to storage (bypasses save method)
func addPlant(_ plant: Plant) {
plants[plant.id] = plant
}
/// Adds multiple plants directly to storage
func addPlants(_ plantsToAdd: [Plant]) {
for plant in plantsToAdd {
plants[plant.id] = plant
}
}
}