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>
This commit is contained in:
245
PlantGuideTests/Mocks/MockPlantClassificationService.swift
Normal file
245
PlantGuideTests/Mocks/MockPlantClassificationService.swift
Normal file
@@ -0,0 +1,245 @@
|
||||
//
|
||||
// MockPlantClassificationService.swift
|
||||
// PlantGuideTests
|
||||
//
|
||||
// Mock implementations for ML-related services for unit testing.
|
||||
// Provides configurable behavior and call tracking for verification.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import UIKit
|
||||
@testable import PlantGuide
|
||||
|
||||
// MARK: - MockPlantClassificationService
|
||||
|
||||
/// Mock implementation of PlantClassificationServiceProtocol for testing
|
||||
final actor MockPlantClassificationService: PlantClassificationServiceProtocol {
|
||||
|
||||
// MARK: - Call Tracking
|
||||
|
||||
private(set) var classifyCallCount = 0
|
||||
|
||||
// MARK: - Error Configuration
|
||||
|
||||
var shouldThrowOnClassify = false
|
||||
var errorToThrow: Error = PlantClassificationError.modelLoadFailed
|
||||
|
||||
// MARK: - Return Value Configuration
|
||||
|
||||
var predictionsToReturn: [PlantPrediction] = []
|
||||
|
||||
// MARK: - Captured Values
|
||||
|
||||
private(set) var lastClassifiedImage: CGImage?
|
||||
|
||||
// MARK: - PlantClassificationServiceProtocol
|
||||
|
||||
func classify(image: CGImage) async throws -> [PlantPrediction] {
|
||||
classifyCallCount += 1
|
||||
lastClassifiedImage = image
|
||||
|
||||
if shouldThrowOnClassify {
|
||||
throw errorToThrow
|
||||
}
|
||||
|
||||
// Return configured predictions or empty array
|
||||
return predictionsToReturn
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Resets all state for clean test setup
|
||||
func reset() {
|
||||
classifyCallCount = 0
|
||||
shouldThrowOnClassify = false
|
||||
errorToThrow = PlantClassificationError.modelLoadFailed
|
||||
predictionsToReturn = []
|
||||
lastClassifiedImage = nil
|
||||
}
|
||||
|
||||
/// Configures the mock to return predictions for common test plants
|
||||
func configureMockPredictions(_ predictions: [PlantPrediction]) {
|
||||
predictionsToReturn = predictions
|
||||
}
|
||||
|
||||
/// Creates a default set of mock predictions
|
||||
func configureDefaultPredictions() {
|
||||
predictionsToReturn = [
|
||||
PlantPrediction(
|
||||
speciesIndex: 0,
|
||||
confidence: 0.92,
|
||||
scientificName: "Monstera deliciosa",
|
||||
commonNames: ["Swiss Cheese Plant", "Monstera"]
|
||||
),
|
||||
PlantPrediction(
|
||||
speciesIndex: 1,
|
||||
confidence: 0.75,
|
||||
scientificName: "Philodendron bipinnatifidum",
|
||||
commonNames: ["Split Leaf Philodendron"]
|
||||
),
|
||||
PlantPrediction(
|
||||
speciesIndex: 2,
|
||||
confidence: 0.45,
|
||||
scientificName: "Epipremnum aureum",
|
||||
commonNames: ["Pothos", "Devil's Ivy"]
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
/// Configures low confidence predictions for testing fallback behavior
|
||||
func configureLowConfidencePredictions() {
|
||||
predictionsToReturn = [
|
||||
PlantPrediction(
|
||||
speciesIndex: 0,
|
||||
confidence: 0.35,
|
||||
scientificName: "Unknown plant",
|
||||
commonNames: ["Unidentified"]
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MockImagePreprocessor
|
||||
|
||||
/// Mock implementation of ImagePreprocessorProtocol for testing
|
||||
struct MockImagePreprocessor: ImagePreprocessorProtocol, Sendable {
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
var shouldThrow = false
|
||||
var errorToThrow: Error = ImagePreprocessorError.cgImageCreationFailed
|
||||
|
||||
// MARK: - Return Value Configuration
|
||||
|
||||
var imageToReturn: CGImage?
|
||||
|
||||
// MARK: - ImagePreprocessorProtocol
|
||||
|
||||
func preprocess(_ image: UIImage) async throws -> CGImage {
|
||||
if shouldThrow {
|
||||
throw errorToThrow
|
||||
}
|
||||
|
||||
// Return configured image or create one from the input
|
||||
if let configuredImage = imageToReturn {
|
||||
return configuredImage
|
||||
}
|
||||
|
||||
guard let cgImage = image.cgImage else {
|
||||
throw ImagePreprocessorError.cgImageCreationFailed
|
||||
}
|
||||
|
||||
return cgImage
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MockIdentifyPlantUseCase
|
||||
|
||||
/// Mock implementation of IdentifyPlantUseCaseProtocol for testing
|
||||
struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
var shouldThrow = false
|
||||
var errorToThrow: Error = IdentifyPlantOnDeviceUseCaseError.noMatchesFound
|
||||
|
||||
// MARK: - Return Value Configuration
|
||||
|
||||
var predictionsToReturn: [ViewPlantPrediction] = []
|
||||
|
||||
// MARK: - IdentifyPlantUseCaseProtocol
|
||||
|
||||
func execute(image: UIImage) async throws -> [ViewPlantPrediction] {
|
||||
if shouldThrow {
|
||||
throw errorToThrow
|
||||
}
|
||||
return predictionsToReturn
|
||||
}
|
||||
|
||||
// MARK: - Factory Methods
|
||||
|
||||
/// Creates a mock that returns high-confidence predictions
|
||||
static func withHighConfidencePredictions() -> MockIdentifyPlantUseCase {
|
||||
var mock = MockIdentifyPlantUseCase()
|
||||
mock.predictionsToReturn = [
|
||||
ViewPlantPrediction(
|
||||
id: UUID(),
|
||||
speciesName: "Monstera deliciosa",
|
||||
commonName: "Swiss Cheese Plant",
|
||||
confidence: 0.95
|
||||
)
|
||||
]
|
||||
return mock
|
||||
}
|
||||
|
||||
/// Creates a mock that returns low-confidence predictions
|
||||
static func withLowConfidencePredictions() -> MockIdentifyPlantUseCase {
|
||||
var mock = MockIdentifyPlantUseCase()
|
||||
mock.predictionsToReturn = [
|
||||
ViewPlantPrediction(
|
||||
id: UUID(),
|
||||
speciesName: "Unknown",
|
||||
commonName: nil,
|
||||
confidence: 0.35
|
||||
)
|
||||
]
|
||||
return mock
|
||||
}
|
||||
|
||||
/// Creates a mock that throws an error
|
||||
static func withError(_ error: Error = IdentifyPlantOnDeviceUseCaseError.noMatchesFound) -> MockIdentifyPlantUseCase {
|
||||
var mock = MockIdentifyPlantUseCase()
|
||||
mock.shouldThrow = true
|
||||
mock.errorToThrow = error
|
||||
return mock
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MockIdentifyPlantOnlineUseCase
|
||||
|
||||
/// Mock implementation of IdentifyPlantOnlineUseCaseProtocol for testing
|
||||
struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Sendable {
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
var shouldThrow = false
|
||||
var errorToThrow: Error = IdentifyPlantOnlineUseCaseError.noMatchesFound
|
||||
|
||||
// MARK: - Return Value Configuration
|
||||
|
||||
var predictionsToReturn: [ViewPlantPrediction] = []
|
||||
|
||||
// MARK: - IdentifyPlantOnlineUseCaseProtocol
|
||||
|
||||
func execute(image: UIImage) async throws -> [ViewPlantPrediction] {
|
||||
if shouldThrow {
|
||||
throw errorToThrow
|
||||
}
|
||||
return predictionsToReturn
|
||||
}
|
||||
|
||||
// MARK: - Factory Methods
|
||||
|
||||
/// Creates a mock that returns API predictions
|
||||
static func withPredictions() -> MockIdentifyPlantOnlineUseCase {
|
||||
var mock = MockIdentifyPlantOnlineUseCase()
|
||||
mock.predictionsToReturn = [
|
||||
ViewPlantPrediction(
|
||||
id: UUID(),
|
||||
speciesName: "Monstera deliciosa",
|
||||
commonName: "Swiss Cheese Plant",
|
||||
confidence: 0.98
|
||||
)
|
||||
]
|
||||
return mock
|
||||
}
|
||||
|
||||
/// Creates a mock that throws an error
|
||||
static func withError(_ error: Error = IdentifyPlantOnlineUseCaseError.noMatchesFound) -> MockIdentifyPlantOnlineUseCase {
|
||||
var mock = MockIdentifyPlantOnlineUseCase()
|
||||
mock.shouldThrow = true
|
||||
mock.errorToThrow = error
|
||||
return mock
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user