Files
PlantGuide/PlantGuideTests/Mocks/MockNetworkService.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

292 lines
7.5 KiB
Swift

//
// MockNetworkService.swift
// PlantGuideTests
//
// Mock implementations for network-related services for unit testing.
// Provides configurable behavior and call tracking for verification.
//
import Foundation
import Network
@testable import PlantGuide
// MARK: - MockNetworkMonitor
/// Mock implementation of NetworkMonitor for testing
/// Note: This creates a testable version that doesn't actually monitor network state
@Observable
final class MockNetworkMonitor: @unchecked Sendable {
// MARK: - Properties
/// Current network connectivity status (configurable for tests)
var isConnected: Bool = true
/// Current connection type (configurable for tests)
var connectionType: ConnectionType = .wifi
// MARK: - Call Tracking
private(set) var startMonitoringCallCount = 0
private(set) var stopMonitoringCallCount = 0
// MARK: - Initialization
init(isConnected: Bool = true, connectionType: ConnectionType = .wifi) {
self.isConnected = isConnected
self.connectionType = connectionType
}
// MARK: - Public Methods
func startMonitoring() {
startMonitoringCallCount += 1
}
func stopMonitoring() {
stopMonitoringCallCount += 1
}
// MARK: - Test Helper Methods
/// Simulates a connection state change
func simulateConnectionChange(isConnected: Bool, connectionType: ConnectionType = .wifi) {
self.isConnected = isConnected
self.connectionType = connectionType
}
/// Simulates going offline
func simulateDisconnect() {
isConnected = false
connectionType = .unknown
}
/// Simulates connecting to WiFi
func simulateWiFiConnection() {
isConnected = true
connectionType = .wifi
}
/// Simulates connecting to cellular
func simulateCellularConnection() {
isConnected = true
connectionType = .cellular
}
/// Resets all state for clean test setup
func reset() {
isConnected = true
connectionType = .wifi
startMonitoringCallCount = 0
stopMonitoringCallCount = 0
}
}
// MARK: - MockURLSession
/// Mock implementation of URLSession for testing network requests
final class MockURLSession: @unchecked Sendable {
// MARK: - Call Tracking
private(set) var dataCallCount = 0
private(set) var uploadCallCount = 0
// MARK: - Error Configuration
var shouldThrowOnData = false
var shouldThrowOnUpload = false
var errorToThrow: Error = NSError(
domain: NSURLErrorDomain,
code: NSURLErrorNotConnectedToInternet,
userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]
)
// MARK: - Return Value Configuration
var dataToReturn: Data = Data()
var responseToReturn: URLResponse?
var statusCodeToReturn: Int = 200
// MARK: - Captured Values
private(set) var lastRequestedURL: URL?
private(set) var lastUploadData: Data?
// MARK: - Mock Methods
func data(from url: URL) async throws -> (Data, URLResponse) {
dataCallCount += 1
lastRequestedURL = url
if shouldThrowOnData {
throw errorToThrow
}
let response = responseToReturn ?? HTTPURLResponse(
url: url,
statusCode: statusCodeToReturn,
httpVersion: "HTTP/1.1",
headerFields: nil
)!
return (dataToReturn, response)
}
func upload(for request: URLRequest, from bodyData: Data) async throws -> (Data, URLResponse) {
uploadCallCount += 1
lastRequestedURL = request.url
lastUploadData = bodyData
if shouldThrowOnUpload {
throw errorToThrow
}
let response = responseToReturn ?? HTTPURLResponse(
url: request.url!,
statusCode: statusCodeToReturn,
httpVersion: "HTTP/1.1",
headerFields: nil
)!
return (dataToReturn, response)
}
// MARK: - Helper Methods
/// Resets all state for clean test setup
func reset() {
dataCallCount = 0
uploadCallCount = 0
shouldThrowOnData = false
shouldThrowOnUpload = false
dataToReturn = Data()
responseToReturn = nil
statusCodeToReturn = 200
lastRequestedURL = nil
lastUploadData = nil
}
/// Configures the mock to return JSON data
func configureJSONResponse<T: Encodable>(_ value: T, statusCode: Int = 200) throws {
let encoder = JSONEncoder()
dataToReturn = try encoder.encode(value)
statusCodeToReturn = statusCode
}
/// Configures the mock to return an error response
func configureErrorResponse(statusCode: Int, message: String = "Error") {
statusCodeToReturn = statusCode
dataToReturn = Data(message.utf8)
}
/// Configures the mock to simulate a network error
func configureNetworkError(_ error: URLError.Code = .notConnectedToInternet) {
shouldThrowOnData = true
shouldThrowOnUpload = true
errorToThrow = URLError(error)
}
/// Configures the mock to simulate a timeout
func configureTimeout() {
shouldThrowOnData = true
shouldThrowOnUpload = true
errorToThrow = URLError(.timedOut)
}
}
// MARK: - MockPlantNetAPIService
/// Mock implementation of PlantNet API service for testing
final class MockPlantNetAPIService: @unchecked Sendable {
// MARK: - PlantNet Response Types
struct PlantNetResponse: Codable {
let results: [PlantNetResult]
}
struct PlantNetResult: Codable {
let score: Double
let species: PlantNetSpecies
}
struct PlantNetSpecies: Codable {
let scientificNameWithoutAuthor: String
let commonNames: [String]
}
// MARK: - Call Tracking
private(set) var identifyCallCount = 0
// MARK: - Error Configuration
var shouldThrow = false
var errorToThrow: Error = NSError(
domain: "PlantNetError",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "Mock PlantNet API error"]
)
// MARK: - Return Value Configuration
var resultsToReturn: [PlantNetResult] = []
// MARK: - Captured Values
private(set) var lastImageData: Data?
// MARK: - Mock Methods
func identify(imageData: Data) async throws -> PlantNetResponse {
identifyCallCount += 1
lastImageData = imageData
if shouldThrow {
throw errorToThrow
}
return PlantNetResponse(results: resultsToReturn)
}
// MARK: - Helper Methods
/// Resets all state for clean test setup
func reset() {
identifyCallCount = 0
shouldThrow = false
resultsToReturn = []
lastImageData = nil
}
/// Configures mock to return successful plant identification
func configureSuccessfulIdentification() {
resultsToReturn = [
PlantNetResult(
score: 0.95,
species: PlantNetSpecies(
scientificNameWithoutAuthor: "Monstera deliciosa",
commonNames: ["Swiss Cheese Plant", "Monstera"]
)
),
PlantNetResult(
score: 0.72,
species: PlantNetSpecies(
scientificNameWithoutAuthor: "Philodendron bipinnatifidum",
commonNames: ["Split Leaf Philodendron"]
)
)
]
}
/// Configures mock to return no results
func configureNoResults() {
resultsToReturn = []
}
}