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

509 lines
18 KiB
Swift

//
// CreateCareScheduleUseCaseTests.swift
// PlantGuideTests
//
// Unit tests for CreateCareScheduleUseCase - the use case for creating plant
// care schedules based on care requirements and user preferences.
//
import XCTest
@testable import PlantGuide
// MARK: - CreateCareScheduleUseCaseTests
final class CreateCareScheduleUseCaseTests: XCTestCase {
// MARK: - Properties
private var sut: CreateCareScheduleUseCase!
// MARK: - Test Lifecycle
override func setUp() {
super.setUp()
sut = CreateCareScheduleUseCase()
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - Test Helpers
private func createBasicCareInfo(
wateringFrequency: WateringFrequency = .weekly,
fertilizerSchedule: FertilizerSchedule? = nil
) -> PlantCareInfo {
PlantCareInfo(
scientificName: "Monstera deliciosa",
commonName: "Swiss Cheese Plant",
lightRequirement: .partialShade,
wateringSchedule: WateringSchedule(frequency: wateringFrequency, amount: .moderate),
temperatureRange: TemperatureRange(minimumCelsius: 18, maximumCelsius: 27),
fertilizerSchedule: fertilizerSchedule
)
}
// MARK: - execute() Basic Schedule Creation Tests
func testExecute_WhenCalled_ReturnsScheduleWithCorrectPlantID() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo()
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
XCTAssertEqual(result.plantID, plant.id)
}
func testExecute_WhenCalled_ReturnsScheduleWithCorrectLightRequirement() async throws {
// Given
let plant = Plant.mock()
let careInfo = PlantCareInfo(
scientificName: "Test Plant",
commonName: nil,
lightRequirement: .fullSun,
wateringSchedule: WateringSchedule(frequency: .weekly, amount: .moderate),
temperatureRange: TemperatureRange(minimumCelsius: 15, maximumCelsius: 30)
)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
XCTAssertEqual(result.lightRequirement, .fullSun)
}
func testExecute_WhenCalled_ReturnsScheduleWithCorrectTemperatureRange() async throws {
// Given
let plant = Plant.mock()
let careInfo = PlantCareInfo(
scientificName: "Test Plant",
commonName: nil,
lightRequirement: .partialShade,
wateringSchedule: WateringSchedule(frequency: .weekly, amount: .moderate),
temperatureRange: TemperatureRange(minimumCelsius: 10, maximumCelsius: 25)
)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
XCTAssertEqual(result.temperatureRange, 10...25)
}
// MARK: - Watering Task Generation Tests
func testExecute_WithWeeklyWatering_GeneratesWateringTasks() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(wateringFrequency: .weekly)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let wateringTasks = result.tasks.filter { $0.type == .watering }
XCTAssertFalse(wateringTasks.isEmpty)
// With 30 days and weekly watering (7-day interval), expect at least 4 tasks
XCTAssertGreaterThanOrEqual(wateringTasks.count, 4)
}
func testExecute_WithDailyWatering_GeneratesMoreFrequentTasks() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(wateringFrequency: .daily)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let wateringTasks = result.tasks.filter { $0.type == .watering }
// With 30 days and daily watering, expect 30 tasks
XCTAssertGreaterThanOrEqual(wateringTasks.count, 30)
}
func testExecute_WithBiweeklyWatering_GeneratesLessFrequentTasks() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(wateringFrequency: .biweekly)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let wateringTasks = result.tasks.filter { $0.type == .watering }
// With 30 days and biweekly watering (14-day interval), expect 2 tasks
XCTAssertGreaterThanOrEqual(wateringTasks.count, 2)
XCTAssertLessThanOrEqual(wateringTasks.count, 3)
}
func testExecute_WateringTasks_HaveCorrectNotes() async throws {
// Given
let plant = Plant.mock()
let careInfo = PlantCareInfo(
scientificName: "Test Plant",
commonName: nil,
lightRequirement: .partialShade,
wateringSchedule: WateringSchedule(frequency: .weekly, amount: .thorough),
temperatureRange: TemperatureRange(minimumCelsius: 18, maximumCelsius: 27)
)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let wateringTasks = result.tasks.filter { $0.type == .watering }
XCTAssertTrue(wateringTasks.allSatisfy { $0.notes?.contains("thorough") ?? false })
}
func testExecute_WateringTasks_HaveCorrectPlantID() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo()
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
XCTAssertTrue(result.tasks.allSatisfy { $0.plantID == plant.id })
}
// MARK: - Fertilizer Task Generation Tests
func testExecute_WithFertilizerSchedule_GeneratesFertilizingTasks() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .monthly, type: .balanced)
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
XCTAssertFalse(fertilizerTasks.isEmpty)
}
func testExecute_WithoutFertilizerSchedule_DoesNotGenerateFertilizingTasks() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(fertilizerSchedule: nil)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
XCTAssertTrue(fertilizerTasks.isEmpty)
}
func testExecute_WithWeeklyFertilizer_GeneratesWeeklyFertilizerTasks() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .organic)
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
// With 30 days and weekly fertilizer (7-day interval), expect at least 4 tasks
XCTAssertGreaterThanOrEqual(fertilizerTasks.count, 4)
}
func testExecute_WithQuarterlyFertilizer_GeneratesSingleFertilizerTask() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .quarterly, type: .balanced)
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
// With 30 days and quarterly fertilizer (90-day interval), expect 1 task
XCTAssertEqual(fertilizerTasks.count, 1)
}
func testExecute_FertilizerTasks_HaveCorrectNotes() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .monthly, type: .highNitrogen)
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
XCTAssertTrue(fertilizerTasks.allSatisfy { $0.notes?.contains("highNitrogen") ?? false })
}
// MARK: - User Preferences Tests
func testExecute_WithPreferredWateringHour_UsesPreferredTime() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(wateringFrequency: .weekly)
let preferences = CarePreferences(preferredWateringHour: 18, preferredWateringMinute: 30)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
// Then
let calendar = Calendar.current
let wateringTasks = result.tasks.filter { $0.type == .watering }
for task in wateringTasks {
let hour = calendar.component(.hour, from: task.scheduledDate)
let minute = calendar.component(.minute, from: task.scheduledDate)
XCTAssertEqual(hour, 18)
XCTAssertEqual(minute, 30)
}
}
func testExecute_WithoutPreferences_UsesDefaultTime() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(wateringFrequency: .weekly)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let calendar = Calendar.current
let wateringTasks = result.tasks.filter { $0.type == .watering }
for task in wateringTasks {
let hour = calendar.component(.hour, from: task.scheduledDate)
XCTAssertEqual(hour, 8) // Default is 8 AM
}
}
func testExecute_WithPreferences_AppliesTimeToFertilizerTasks() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .balanced)
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
let preferences = CarePreferences(preferredWateringHour: 9, preferredWateringMinute: 15)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
// Then
let calendar = Calendar.current
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
for task in fertilizerTasks {
let hour = calendar.component(.hour, from: task.scheduledDate)
let minute = calendar.component(.minute, from: task.scheduledDate)
XCTAssertEqual(hour, 9)
XCTAssertEqual(minute, 15)
}
}
// MARK: - Task Scheduling Tests
func testExecute_TasksStartFromTomorrow() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo()
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today)!
for task in result.tasks {
let taskDay = calendar.startOfDay(for: task.scheduledDate)
XCTAssertGreaterThanOrEqual(taskDay, tomorrow)
}
}
func testExecute_TasksAreSortedByDate() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .balanced)
let careInfo = createBasicCareInfo(
wateringFrequency: .twiceWeekly,
fertilizerSchedule: fertilizerSchedule
)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
for index in 0..<(result.tasks.count - 1) {
XCTAssertLessThanOrEqual(
result.tasks[index].scheduledDate,
result.tasks[index + 1].scheduledDate
)
}
}
func testExecute_TasksHaveUniqueIDs() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .balanced)
let careInfo = createBasicCareInfo(
wateringFrequency: .daily,
fertilizerSchedule: fertilizerSchedule
)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let taskIDs = result.tasks.map { $0.id }
let uniqueIDs = Set(taskIDs)
XCTAssertEqual(taskIDs.count, uniqueIDs.count, "All task IDs should be unique")
}
// MARK: - Schedule Metadata Tests
func testExecute_WateringScheduleString_MatchesFrequency() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(wateringFrequency: .biweekly)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
XCTAssertEqual(result.wateringSchedule, "biweekly")
}
func testExecute_FertilizerScheduleString_WhenNoFertilizer_ReturnsNotRequired() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(fertilizerSchedule: nil)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
XCTAssertEqual(result.fertilizerSchedule, "Not required")
}
func testExecute_FertilizerScheduleString_WithFertilizer_ReturnsFrequency() async throws {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: .monthly, type: .organic)
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
XCTAssertEqual(result.fertilizerSchedule, "monthly")
}
// MARK: - Protocol Conformance Tests
func testCreateCareScheduleUseCase_ConformsToProtocol() {
XCTAssertTrue(sut is CreateCareScheduleUseCaseProtocol)
}
// MARK: - Edge Cases
func testExecute_WithAllFertilizerFrequencies_GeneratesCorrectTaskCounts() async throws {
let frequencies: [(FertilizerFrequency, Int)] = [
(.weekly, 4), // 30 / 7 = at least 4
(.biweekly, 2), // 30 / 14 = 2
(.monthly, 1), // 30 / 30 = 1
(.quarterly, 1), // 30 / 90 = 1 (minimum 1)
(.biannually, 1) // 30 / 182 = 1 (minimum 1)
]
for (frequency, expectedMinCount) in frequencies {
// Given
let plant = Plant.mock()
let fertilizerSchedule = FertilizerSchedule(frequency: frequency, type: .balanced)
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
XCTAssertGreaterThanOrEqual(
fertilizerTasks.count,
expectedMinCount,
"Expected at least \(expectedMinCount) tasks for \(frequency.rawValue) frequency"
)
}
}
func testExecute_WithAllWateringFrequencies_GeneratesCorrectTaskCounts() async throws {
let frequencies: [(WateringFrequency, Int)] = [
(.daily, 30), // 30 / 1 = 30
(.everyOtherDay, 15), // 30 / 2 = 15
(.twiceWeekly, 10), // 30 / 3 = 10
(.weekly, 4), // 30 / 7 = 4
(.biweekly, 2), // 30 / 14 = 2
(.monthly, 1) // 30 / 30 = 1
]
for (frequency, expectedMinCount) in frequencies {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo(wateringFrequency: frequency)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
// Then
let wateringTasks = result.tasks.filter { $0.type == .watering }
XCTAssertGreaterThanOrEqual(
wateringTasks.count,
expectedMinCount,
"Expected at least \(expectedMinCount) tasks for \(frequency.rawValue) frequency"
)
}
}
func testExecute_WithMidnightPreferredTime_GeneratesTasksAtMidnight() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo()
let preferences = CarePreferences(preferredWateringHour: 0, preferredWateringMinute: 0)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
// Then
let calendar = Calendar.current
for task in result.tasks {
let hour = calendar.component(.hour, from: task.scheduledDate)
let minute = calendar.component(.minute, from: task.scheduledDate)
XCTAssertEqual(hour, 0)
XCTAssertEqual(minute, 0)
}
}
func testExecute_WithLateNightPreferredTime_GeneratesTasksAtLateNight() async throws {
// Given
let plant = Plant.mock()
let careInfo = createBasicCareInfo()
let preferences = CarePreferences(preferredWateringHour: 23, preferredWateringMinute: 59)
// When
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
// Then
let calendar = Calendar.current
for task in result.tasks {
let hour = calendar.component(.hour, from: task.scheduledDate)
let minute = calendar.component(.minute, from: task.scheduledDate)
XCTAssertEqual(hour, 23)
XCTAssertEqual(minute, 59)
}
}
}