- 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>
361 lines
9.4 KiB
Swift
361 lines
9.4 KiB
Swift
//
|
|
// NetworkMonitorTests.swift
|
|
// PlantGuideTests
|
|
//
|
|
// Unit tests for NetworkMonitor - the network connectivity monitoring service
|
|
// that tracks network status and connection type changes.
|
|
//
|
|
|
|
import XCTest
|
|
import Network
|
|
@testable import PlantGuide
|
|
|
|
// MARK: - NetworkMonitorTests
|
|
|
|
final class NetworkMonitorTests: XCTestCase {
|
|
|
|
// MARK: - Properties
|
|
|
|
private var sut: NetworkMonitor!
|
|
|
|
// MARK: - Test Lifecycle
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
sut = NetworkMonitor()
|
|
}
|
|
|
|
override func tearDown() {
|
|
sut?.stopMonitoring()
|
|
sut = nil
|
|
super.tearDown()
|
|
}
|
|
|
|
// MARK: - Initialization Tests
|
|
|
|
func testInit_CreatesMonitor() {
|
|
XCTAssertNotNil(sut)
|
|
}
|
|
|
|
func testInit_StartsMonitoringAutomatically() {
|
|
// NetworkMonitor starts monitoring in init
|
|
// Just verify it doesn't crash and is created
|
|
XCTAssertNotNil(sut)
|
|
}
|
|
|
|
// MARK: - Connection Status Tests
|
|
|
|
func testIsConnected_InitialValue_IsBool() {
|
|
// Just verify the property is accessible and is a boolean
|
|
let isConnected = sut.isConnected
|
|
XCTAssertTrue(isConnected || !isConnected) // Always true - just checks type
|
|
}
|
|
|
|
func testConnectionType_InitialValue_IsConnectionType() {
|
|
// Verify the property is accessible and has a valid value
|
|
let connectionType = sut.connectionType
|
|
let validTypes: [ConnectionType] = [.wifi, .cellular, .ethernet, .unknown]
|
|
XCTAssertTrue(validTypes.contains(connectionType))
|
|
}
|
|
|
|
// MARK: - Start/Stop Monitoring Tests
|
|
|
|
func testStartMonitoring_WhenAlreadyStarted_DoesNotCrash() {
|
|
// Given - Already started in init
|
|
|
|
// When - Start again
|
|
sut.startMonitoring()
|
|
|
|
// Then - Should not crash
|
|
XCTAssertNotNil(sut)
|
|
}
|
|
|
|
func testStopMonitoring_WhenMonitoring_StopsSuccessfully() {
|
|
// Given - Already started in init
|
|
|
|
// When
|
|
sut.stopMonitoring()
|
|
|
|
// Then - Should not crash
|
|
XCTAssertNotNil(sut)
|
|
}
|
|
|
|
func testStopMonitoring_WhenAlreadyStopped_DoesNotCrash() {
|
|
// Given
|
|
sut.stopMonitoring()
|
|
|
|
// When - Stop again
|
|
sut.stopMonitoring()
|
|
|
|
// Then - Should not crash
|
|
XCTAssertNotNil(sut)
|
|
}
|
|
|
|
func testStartMonitoring_AfterStop_RestartsSuccessfully() {
|
|
// Given
|
|
sut.stopMonitoring()
|
|
|
|
// When
|
|
sut.startMonitoring()
|
|
|
|
// Then - Should not crash
|
|
XCTAssertNotNil(sut)
|
|
}
|
|
|
|
// MARK: - ConnectionType Tests
|
|
|
|
func testConnectionType_WiFi_HasCorrectRawValue() {
|
|
XCTAssertEqual(ConnectionType.wifi.rawValue, "wifi")
|
|
}
|
|
|
|
func testConnectionType_Cellular_HasCorrectRawValue() {
|
|
XCTAssertEqual(ConnectionType.cellular.rawValue, "cellular")
|
|
}
|
|
|
|
func testConnectionType_Ethernet_HasCorrectRawValue() {
|
|
XCTAssertEqual(ConnectionType.ethernet.rawValue, "ethernet")
|
|
}
|
|
|
|
func testConnectionType_Unknown_HasCorrectRawValue() {
|
|
XCTAssertEqual(ConnectionType.unknown.rawValue, "unknown")
|
|
}
|
|
|
|
// MARK: - Thread Safety Tests
|
|
|
|
func testConcurrentAccess_DoesNotCrash() {
|
|
// Given
|
|
let expectation = XCTestExpectation(description: "Concurrent access completes")
|
|
expectation.expectedFulfillmentCount = 100
|
|
|
|
// When - Access from multiple threads
|
|
for _ in 0..<100 {
|
|
DispatchQueue.global().async {
|
|
_ = self.sut.isConnected
|
|
_ = self.sut.connectionType
|
|
expectation.fulfill()
|
|
}
|
|
}
|
|
|
|
// Then
|
|
wait(for: [expectation], timeout: 5.0)
|
|
}
|
|
|
|
func testConcurrentStartStop_DoesNotCrash() {
|
|
// Given
|
|
let expectation = XCTestExpectation(description: "Concurrent start/stop completes")
|
|
expectation.expectedFulfillmentCount = 20
|
|
|
|
// When - Start and stop from multiple threads
|
|
for i in 0..<20 {
|
|
DispatchQueue.global().async {
|
|
if i % 2 == 0 {
|
|
self.sut.startMonitoring()
|
|
} else {
|
|
self.sut.stopMonitoring()
|
|
}
|
|
expectation.fulfill()
|
|
}
|
|
}
|
|
|
|
// Then
|
|
wait(for: [expectation], timeout: 5.0)
|
|
}
|
|
|
|
// MARK: - Lifecycle Tests
|
|
|
|
func testDeinit_StopsMonitoring() {
|
|
// Given
|
|
var monitor: NetworkMonitor? = NetworkMonitor()
|
|
XCTAssertNotNil(monitor)
|
|
|
|
// When
|
|
monitor = nil
|
|
|
|
// Then - Should not crash (deinit calls stopMonitoring)
|
|
XCTAssertNil(monitor)
|
|
}
|
|
|
|
func testMultipleInstances_DoNotInterfere() {
|
|
// Given
|
|
let monitor1 = NetworkMonitor()
|
|
let monitor2 = NetworkMonitor()
|
|
|
|
// When
|
|
monitor1.stopMonitoring()
|
|
|
|
// Then - monitor2 should still work
|
|
XCTAssertNotNil(monitor2)
|
|
let isConnected = monitor2.isConnected
|
|
XCTAssertTrue(isConnected || !isConnected) // Just verify access works
|
|
}
|
|
|
|
// MARK: - Observable Property Tests
|
|
|
|
func testIsConnected_CanBeObserved() {
|
|
// Given
|
|
let expectation = XCTestExpectation(description: "Property can be read")
|
|
|
|
// When
|
|
DispatchQueue.main.async {
|
|
_ = self.sut.isConnected
|
|
expectation.fulfill()
|
|
}
|
|
|
|
// Then
|
|
wait(for: [expectation], timeout: 1.0)
|
|
}
|
|
|
|
func testConnectionType_CanBeObserved() {
|
|
// Given
|
|
let expectation = XCTestExpectation(description: "Property can be read")
|
|
|
|
// When
|
|
DispatchQueue.main.async {
|
|
_ = self.sut.connectionType
|
|
expectation.fulfill()
|
|
}
|
|
|
|
// Then
|
|
wait(for: [expectation], timeout: 1.0)
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
func testRapidStartStop_DoesNotCrash() {
|
|
// Rapidly toggle monitoring
|
|
for _ in 0..<50 {
|
|
sut.startMonitoring()
|
|
sut.stopMonitoring()
|
|
}
|
|
|
|
// Should not crash
|
|
XCTAssertNotNil(sut)
|
|
}
|
|
|
|
func testNewInstance_AfterOldOneDeallocated_Works() {
|
|
// Given
|
|
var monitor: NetworkMonitor? = NetworkMonitor()
|
|
monitor?.stopMonitoring()
|
|
monitor = nil
|
|
|
|
// When
|
|
let newMonitor = NetworkMonitor()
|
|
|
|
// Then
|
|
XCTAssertNotNil(newMonitor)
|
|
let isConnected = newMonitor.isConnected
|
|
XCTAssertTrue(isConnected || !isConnected)
|
|
|
|
newMonitor.stopMonitoring()
|
|
}
|
|
|
|
// MARK: - Integration Tests
|
|
|
|
func testNetworkMonitor_WorksWithActualNetwork() async throws {
|
|
// This test verifies that the monitor works with the actual network
|
|
// It's an integration test that depends on the device's network state
|
|
|
|
// Wait a moment for the monitor to update
|
|
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
|
|
|
// Just verify we can read the values without crashing
|
|
let isConnected = sut.isConnected
|
|
let connectionType = sut.connectionType
|
|
|
|
// Log the actual values for debugging
|
|
print("Network connected: \(isConnected)")
|
|
print("Connection type: \(connectionType.rawValue)")
|
|
|
|
// Verify we got valid values
|
|
XCTAssertTrue(isConnected || !isConnected)
|
|
let validTypes: [ConnectionType] = [.wifi, .cellular, .ethernet, .unknown]
|
|
XCTAssertTrue(validTypes.contains(connectionType))
|
|
}
|
|
|
|
// MARK: - Memory Tests
|
|
|
|
func testNoMemoryLeak_WhenCreatedAndDestroyed() {
|
|
// Given
|
|
weak var weakMonitor: NetworkMonitor?
|
|
|
|
autoreleasepool {
|
|
let monitor = NetworkMonitor()
|
|
weakMonitor = monitor
|
|
monitor.stopMonitoring()
|
|
}
|
|
|
|
// Note: Due to internal dispatch queues and NWPathMonitor,
|
|
// the monitor may not be immediately deallocated.
|
|
// This test primarily verifies no crash occurs.
|
|
XCTAssertTrue(true)
|
|
}
|
|
}
|
|
|
|
// MARK: - MockNetworkMonitor Tests
|
|
|
|
/// Tests for the MockNetworkMonitor used in other tests
|
|
final class MockNetworkMonitorTests: XCTestCase {
|
|
|
|
// MARK: - Properties
|
|
|
|
private var mockMonitor: MockNetworkMonitor!
|
|
|
|
// MARK: - Test Lifecycle
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
mockMonitor = MockNetworkMonitor(isConnected: true)
|
|
}
|
|
|
|
override func tearDown() {
|
|
mockMonitor = nil
|
|
super.tearDown()
|
|
}
|
|
|
|
// MARK: - Mock Behavior Tests
|
|
|
|
func testMockNetworkMonitor_WhenInitializedConnected_ReportsConnected() {
|
|
// Given
|
|
let monitor = MockNetworkMonitor(isConnected: true)
|
|
|
|
// Then
|
|
XCTAssertTrue(monitor.isConnected)
|
|
}
|
|
|
|
func testMockNetworkMonitor_WhenInitializedDisconnected_ReportsDisconnected() {
|
|
// Given
|
|
let monitor = MockNetworkMonitor(isConnected: false)
|
|
|
|
// Then
|
|
XCTAssertFalse(monitor.isConnected)
|
|
}
|
|
|
|
func testMockNetworkMonitor_CanChangeConnectionStatus() {
|
|
// Given
|
|
let monitor = MockNetworkMonitor(isConnected: true)
|
|
XCTAssertTrue(monitor.isConnected)
|
|
|
|
// When
|
|
monitor.isConnected = false
|
|
|
|
// Then
|
|
XCTAssertFalse(monitor.isConnected)
|
|
}
|
|
|
|
func testMockNetworkMonitor_TrackStartMonitoringCalls() {
|
|
// When
|
|
mockMonitor.startMonitoring()
|
|
|
|
// Then
|
|
XCTAssertEqual(mockMonitor.startMonitoringCallCount, 1)
|
|
}
|
|
|
|
func testMockNetworkMonitor_TrackStopMonitoringCalls() {
|
|
// When
|
|
mockMonitor.stopMonitoring()
|
|
|
|
// Then
|
|
XCTAssertEqual(mockMonitor.stopMonitoringCallCount, 1)
|
|
}
|
|
}
|