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>
This commit is contained in:
360
PlantGuideTests/NetworkMonitorTests.swift
Normal file
360
PlantGuideTests/NetworkMonitorTests.swift
Normal file
@@ -0,0 +1,360 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user