Files
PlantGuide/PlantGuideTests/Mocks/MockPlantClassificationService.swift
Trey t 1ae9c884c8 Rebuild UI test foundation with page objects, wait helpers, and screen objects
Replace brittle localized-string selectors and broken wait helpers with a
robust, identifier-first UI test infrastructure. All 41 UI tests pass on
iOS 26.2 simulator (iPhone 17).

Foundation:
- BaseUITestCase with deterministic launch helpers (launchClean, launchOffline)
- WaitHelpers (waitUntilHittable, waitUntilGone, tapWhenReady) replacing sleep()
- UITestID enum mirroring AccessibilityIdentifiers from the app target
- Screen objects: TabBarScreen, CameraScreen, CollectionScreen, TodayScreen,
  SettingsScreen, PlantDetailScreen

Key fixes:
- Tab navigation uses waitForExistence+tap instead of isHittable (unreliable
  in iOS 26 simulator)
- Tests handle real app state (empty collection, no camera permission)
- Increased timeouts for parallel clone execution
- Added NetworkMonitorProtocol and protocol-typed DI for testability
- Fixed actor-isolation issues in unit test mocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:36:54 -06:00

252 lines
7.2 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 throw behavior from outside the actor
func setThrowBehavior(shouldThrow: Bool, error: Error) {
shouldThrowOnClassify = shouldThrow
errorToThrow = error
}
/// 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
}
}