Base fixes and test harness
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import CoreData
|
||||
@testable import PortfolioJournal
|
||||
|
||||
/// Error types for TestCoreDataStack
|
||||
enum TestCoreDataStackError: Error {
|
||||
case modelNotFound
|
||||
case storeLoadFailed(Error)
|
||||
}
|
||||
|
||||
/// In-memory Core Data stack for testing purposes
|
||||
/// This allows tests to run in isolation without affecting the actual database
|
||||
final class TestCoreDataStack {
|
||||
|
||||
let persistentContainer: NSPersistentContainer
|
||||
|
||||
var viewContext: NSManagedObjectContext {
|
||||
persistentContainer.viewContext
|
||||
}
|
||||
|
||||
init() throws {
|
||||
// Use the same approach as NSPersistentContainer(name:) which auto-finds the model
|
||||
// When tests run with the app as TEST_HOST, Bundle.main is the app bundle
|
||||
let modelName = "PortfolioJournal"
|
||||
|
||||
// Create the container - it will find the model in the main bundle
|
||||
persistentContainer = NSPersistentContainer(name: modelName)
|
||||
|
||||
// Configure for in-memory storage
|
||||
let description = NSPersistentStoreDescription()
|
||||
description.type = NSInMemoryStoreType
|
||||
description.shouldAddStoreAsynchronously = false
|
||||
|
||||
persistentContainer.persistentStoreDescriptions = [description]
|
||||
|
||||
var loadError: Error?
|
||||
persistentContainer.loadPersistentStores { _, error in
|
||||
loadError = error
|
||||
}
|
||||
|
||||
if let error = loadError {
|
||||
throw TestCoreDataStackError.storeLoadFailed(error)
|
||||
}
|
||||
|
||||
viewContext.automaticallyMergesChangesFromParent = true
|
||||
viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
}
|
||||
|
||||
/// Creates a fresh context for each test
|
||||
func newBackgroundContext() -> NSManagedObjectContext {
|
||||
let context = persistentContainer.newBackgroundContext()
|
||||
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
return context
|
||||
}
|
||||
|
||||
/// Resets the in-memory store (call in tearDown)
|
||||
func reset() {
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Snapshot")
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
try? viewContext.execute(deleteRequest)
|
||||
|
||||
let sourceFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "InvestmentSource")
|
||||
let sourceDelete = NSBatchDeleteRequest(fetchRequest: sourceFetch)
|
||||
try? viewContext.execute(sourceDelete)
|
||||
|
||||
let categoryFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Category")
|
||||
let categoryDelete = NSBatchDeleteRequest(fetchRequest: categoryFetch)
|
||||
try? viewContext.execute(categoryDelete)
|
||||
|
||||
let accountFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Account")
|
||||
let accountDelete = NSBatchDeleteRequest(fetchRequest: accountFetch)
|
||||
try? viewContext.execute(accountDelete)
|
||||
|
||||
let goalFetch: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Goal")
|
||||
let goalDelete = NSBatchDeleteRequest(fetchRequest: goalFetch)
|
||||
try? viewContext.execute(goalDelete)
|
||||
|
||||
viewContext.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Data Factory
|
||||
|
||||
extension TestCoreDataStack {
|
||||
|
||||
/// Creates a test account
|
||||
@MainActor
|
||||
func createTestAccount(
|
||||
name: String = "Test Account",
|
||||
currency: String = "USD"
|
||||
) -> Account {
|
||||
let account = Account(context: viewContext)
|
||||
account.id = UUID()
|
||||
account.name = name
|
||||
account.currency = currency
|
||||
account.createdAt = Date()
|
||||
return account
|
||||
}
|
||||
|
||||
/// Creates a test category
|
||||
@MainActor
|
||||
func createTestCategory(
|
||||
name: String = "Test Category",
|
||||
colorHex: String = "#3B82F6"
|
||||
) -> PortfolioJournal.Category {
|
||||
let category = PortfolioJournal.Category(context: viewContext)
|
||||
category.id = UUID()
|
||||
category.name = name
|
||||
category.colorHex = colorHex
|
||||
return category
|
||||
}
|
||||
|
||||
/// Creates a test investment source
|
||||
@MainActor
|
||||
func createTestSource(
|
||||
name: String = "Test Source",
|
||||
account: Account? = nil,
|
||||
category: PortfolioJournal.Category? = nil
|
||||
) -> InvestmentSource {
|
||||
let source = InvestmentSource(context: viewContext)
|
||||
source.id = UUID()
|
||||
source.name = name
|
||||
source.createdAt = Date()
|
||||
source.account = account
|
||||
source.category = category
|
||||
return source
|
||||
}
|
||||
|
||||
/// Creates a test snapshot
|
||||
@MainActor
|
||||
func createTestSnapshot(
|
||||
source: InvestmentSource,
|
||||
value: Decimal,
|
||||
date: Date = Date(),
|
||||
contribution: Decimal? = nil
|
||||
) -> Snapshot {
|
||||
let snapshot = Snapshot(context: viewContext)
|
||||
snapshot.id = UUID()
|
||||
snapshot.value = NSDecimalNumber(decimal: value)
|
||||
snapshot.date = date
|
||||
snapshot.source = source
|
||||
if let contribution = contribution {
|
||||
snapshot.contribution = NSDecimalNumber(decimal: contribution)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/// Creates a test goal
|
||||
@MainActor
|
||||
func createTestGoal(
|
||||
name: String = "Test Goal",
|
||||
targetAmount: Decimal,
|
||||
account: Account? = nil,
|
||||
targetDate: Date? = nil
|
||||
) -> Goal {
|
||||
let goal = Goal(context: viewContext)
|
||||
goal.id = UUID()
|
||||
goal.name = name
|
||||
goal.targetAmount = NSDecimalNumber(decimal: targetAmount)
|
||||
goal.createdAt = Date()
|
||||
goal.isActive = true
|
||||
goal.account = account
|
||||
goal.targetDate = targetDate
|
||||
return goal
|
||||
}
|
||||
|
||||
/// Saves the context
|
||||
func save() throws {
|
||||
if viewContext.hasChanges {
|
||||
try viewContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user