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>
292 lines
7.5 KiB
Swift
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: NetworkMonitorProtocol, @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 = []
|
|
}
|
|
}
|