- 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>
246 lines
7.0 KiB
Swift
246 lines
7.0 KiB
Swift
//
|
|
// 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
|
|
}
|
|
}
|