# Testing and Debugging Patterns for unit testing, UI testing, and debugging macOS apps. ```swift import XCTest @testable import MyApp final class DataServiceTests: XCTestCase { var sut: DataService! override func setUp() { super.setUp() sut = DataService() } override func tearDown() { sut = nil super.tearDown() } func testAddItem() { // Given let item = Item(name: "Test") // When sut.addItem(item) // Then XCTAssertEqual(sut.items.count, 1) XCTAssertEqual(sut.items.first?.name, "Test") } func testDeleteItem() { // Given let item = Item(name: "Test") sut.addItem(item) // When sut.deleteItem(item.id) // Then XCTAssertTrue(sut.items.isEmpty) } } ``` ```swift final class NetworkServiceTests: XCTestCase { var sut: NetworkService! var mockSession: MockURLSession! override func setUp() { super.setUp() mockSession = MockURLSession() sut = NetworkService(session: mockSession) } func testFetchProjects() async throws { // Given let expectedProjects = [Project(name: "Test")] mockSession.data = try JSONEncoder().encode(expectedProjects) mockSession.response = HTTPURLResponse( url: URL(string: "https://api.example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil ) // When let projects: [Project] = try await sut.fetch(Endpoint.projects().request) // Then XCTAssertEqual(projects.count, 1) XCTAssertEqual(projects.first?.name, "Test") } func testFetchError() async { // Given mockSession.error = NetworkError.timeout // When/Then do { let _: [Project] = try await sut.fetch(Endpoint.projects().request) XCTFail("Expected error") } catch { XCTAssertTrue(error is NetworkError) } } } ``` ```swift final class AppStateTests: XCTestCase { func testAddItem() { // Given let sut = AppState() // When sut.addItem(Item(name: "Test")) // Then XCTAssertEqual(sut.items.count, 1) } func testSelectedItem() { // Given let sut = AppState() let item = Item(name: "Test") sut.items = [item] // When sut.selectedItemID = item.id // Then XCTAssertEqual(sut.selectedItem?.name, "Test") } } ``` ```swift // Protocol for testability protocol DataStoreProtocol { func fetchAll() async throws -> [Item] func save(_ item: Item) async throws } // Mock implementation class MockDataStore: DataStoreProtocol { var itemsToReturn: [Item] = [] var savedItems: [Item] = [] var shouldThrow = false func fetchAll() async throws -> [Item] { if shouldThrow { throw TestError.mock } return itemsToReturn } func save(_ item: Item) async throws { if shouldThrow { throw TestError.mock } savedItems.append(item) } } enum TestError: Error { case mock } // Test using mock final class ViewModelTests: XCTestCase { func testLoadItems() async throws { // Given let mockStore = MockDataStore() mockStore.itemsToReturn = [Item(name: "Test")] let sut = ViewModel(dataStore: mockStore) // When await sut.loadItems() // Then XCTAssertEqual(sut.items.count, 1) } } ``` ```swift final class SwiftDataTests: XCTestCase { var container: ModelContainer! var context: ModelContext! override func setUp() { super.setUp() let schema = Schema([Project.self, Task.self]) let config = ModelConfiguration(isStoredInMemoryOnly: true) container = try! ModelContainer(for: schema, configurations: config) context = ModelContext(container) } func testCreateProject() throws { // Given let project = Project(name: "Test") // When context.insert(project) try context.save() // Then let descriptor = FetchDescriptor() let projects = try context.fetch(descriptor) XCTAssertEqual(projects.count, 1) XCTAssertEqual(projects.first?.name, "Test") } func testCascadeDelete() throws { // Given let project = Project(name: "Test") let task = Task(title: "Task") task.project = project context.insert(project) context.insert(task) try context.save() // When context.delete(project) try context.save() // Then let tasks = try context.fetch(FetchDescriptor()) XCTAssertTrue(tasks.isEmpty) } } ``` When SwiftData items aren't appearing or relationships seem broken: ```swift // Debug print to verify relationships func debugRelationships(for column: Column) { print("=== Column: \(column.name) ===") print("Cards count: \(column.cards.count)") for card in column.cards { print(" - Card: \(card.title)") print(" Card's column: \(card.column?.name ?? "NIL")") } } // Verify inverse relationships are set func verifyCard(_ card: Card) { if card.column == nil { print("⚠️ Card '\(card.title)' has no column set!") } else { let inParentArray = card.column!.cards.contains { $0.id == card.id } print("Card in column.cards: \(inParentArray)") } } ``` **Issue: Items not appearing in list** Symptoms: Added items don't show, count is 0 Debug steps: ```swift // 1. Check modelContext has the item let descriptor = FetchDescriptor() let allCards = try? modelContext.fetch(descriptor) print("Total cards in context: \(allCards?.count ?? 0)") // 2. Check relationship is set if let card = allCards?.first { print("Card column: \(card.column?.name ?? "NIL")") } // 3. Check parent's array print("Column.cards count: \(column.cards.count)") ``` Common causes: - Forgot `modelContext.insert(item)` for new objects - Didn't set inverse relationship (`card.column = column`) - Using wrong modelContext (view context vs background context) ```swift // Print database location func printDatabaseLocation() { let url = URL.applicationSupportDirectory .appendingPathComponent("default.store") print("Database: \(url.path)") } // Dump all items of a type func dumpAllItems(_ type: T.Type, context: ModelContext) { let descriptor = FetchDescriptor() if let items = try? context.fetch(descriptor) { print("=== \(String(describing: T.self)) (\(items.count)) ===") for item in items { print(" \(item)") } } } // Usage dumpAllItems(Column.self, context: modelContext) dumpAllItems(Card.self, context: modelContext) ``` ```swift import os let dataLogger = Logger(subsystem: "com.yourapp", category: "SwiftData") // Log when adding items func addCard(to column: Column, title: String) { let card = Card(title: title, position: 1.0) card.column = column modelContext.insert(card) dataLogger.debug("Added card '\(title)' to column '\(column.name)'") dataLogger.debug("Column now has \(column.cards.count) cards") } // Log when relationships change func moveCard(_ card: Card, to newColumn: Column) { let oldColumn = card.column?.name ?? "none" card.column = newColumn dataLogger.debug("Moved '\(card.title)' from '\(oldColumn)' to '\(newColumn.name)'") } // View logs in Console.app or: // log stream --predicate 'subsystem == "com.yourapp" AND category == "SwiftData"' --level debug ``` **Quick reference for common SwiftData symptoms:** | Symptom | Likely Cause | Fix | |---------|--------------|-----| | Items don't appear | Missing `insert()` | Call `modelContext.insert(item)` | | Items appear once then disappear | Inverse relationship not set | Set `child.parent = parent` before insert | | Changes don't persist | Wrong context | Use same modelContext throughout | | @Query returns empty | Schema mismatch | Verify @Model matches container schema | | Cascade delete fails | Missing deleteRule | Add `@Relationship(deleteRule: .cascade)` | | Relationship array always empty | Not using inverse | Set inverse on child, not append on parent | ```swift import XCTest final class MyAppUITests: XCTestCase { var app: XCUIApplication! override func setUp() { super.setUp() continueAfterFailure = false app = XCUIApplication() app.launch() } func testAddItem() { // Tap add button app.buttons["Add"].click() // Verify item appears in list XCTAssertTrue(app.staticTexts["New Item"].exists) } func testRenameItem() { // Add item first app.buttons["Add"].click() // Select and rename app.staticTexts["New Item"].click() let textField = app.textFields["Name"] textField.click() textField.typeText("Renamed Item") // Verify XCTAssertTrue(app.staticTexts["Renamed Item"].exists) } func testDeleteItem() { // Add item app.buttons["Add"].click() // Right-click and delete app.staticTexts["New Item"].rightClick() app.menuItems["Delete"].click() // Verify deleted XCTAssertFalse(app.staticTexts["New Item"].exists) } } ``` ```swift import os let logger = Logger(subsystem: "com.yourcompany.MyApp", category: "General") // Log levels logger.debug("Debug info") logger.info("General info") logger.notice("Notable event") logger.error("Error occurred") logger.fault("Critical failure") // With interpolation logger.info("Loaded \(items.count) items") // Privacy for sensitive data logger.info("User: \(username, privacy: .private)") // In console // log stream --predicate 'subsystem == "com.yourcompany.MyApp"' --level debug ``` ```swift import os let signposter = OSSignposter(subsystem: "com.yourcompany.MyApp", category: "Performance") func loadData() async { let signpostID = signposter.makeSignpostID() let state = signposter.beginInterval("Load Data", id: signpostID) // Work await fetchFromNetwork() signposter.endInterval("Load Data", state) } // Interval with metadata func processItem(_ item: Item) { let state = signposter.beginInterval("Process Item", id: signposter.makeSignpostID()) // Work process(item) signposter.endInterval("Process Item", state, "Processed \(item.name)") } ``` ```swift // Symbolic breakpoints in Xcode: // - Symbol: `-[NSException raise]` to catch all exceptions // - Symbol: `UIViewAlertForUnsatisfiableConstraints` for layout issues // In code, trigger debugger func criticalFunction() { guard condition else { #if DEBUG raise(SIGINT) // Triggers breakpoint #endif return } } ``` ```swift // Check for leaks with weak references class DebugHelper { static func trackDeallocation(_ object: T, name: String) { let observer = DeallocObserver(name: name) objc_setAssociatedObject(object, "deallocObserver", observer, .OBJC_ASSOCIATION_RETAIN) } } class DeallocObserver { let name: String init(name: String) { self.name = name } deinit { print("✓ \(name) deallocated") } } // Usage in tests func testNoMemoryLeak() { weak var weakRef: ViewModel? autoreleasepool { let vm = ViewModel() weakRef = vm DebugHelper.trackDeallocation(vm, name: "ViewModel") } XCTAssertNil(weakRef, "ViewModel should be deallocated") } ``` **Symptom**: Memory grows over time, objects not deallocated **Common causes**: - Strong reference cycles in closures - Delegate not weak - NotificationCenter observers not removed **Fix**: ```swift // Use [weak self] someService.fetch { [weak self] result in self?.handle(result) } // Weak delegates weak var delegate: MyDelegate? // Remove observers deinit { NotificationCenter.default.removeObserver(self) } ``` **Symptom**: Purple warnings, UI not updating, crashes **Fix**: ```swift // Ensure UI updates on main thread Task { @MainActor in self.items = fetchedItems } // Or use DispatchQueue DispatchQueue.main.async { self.tableView.reloadData() } ``` **Symptom**: View doesn't reflect state changes **Common causes**: - Missing @Observable - Property not being tracked - Binding not connected **Fix**: ```swift // Ensure class is @Observable @Observable class AppState { var items: [Item] = [] // This will be tracked } // Use @Bindable for mutations @Bindable var appState = appState TextField("Name", text: $appState.name) ``` ```bash # Build with coverage xcodebuild -project MyApp.xcodeproj \ -scheme MyApp \ -enableCodeCoverage YES \ -derivedDataPath ./build \ test # View coverage report xcrun xccov view --report ./build/Logs/Test/*.xcresult ``` ```swift func testPerformanceLoadLargeDataset() { measure { let items = (0..<10000).map { Item(name: "Item \($0)") } sut.items = items } } // With options func testPerformanceWithMetrics() { let metrics: [XCTMetric] = [ XCTClockMetric(), XCTMemoryMetric(), XCTCPUMetric() ] measure(metrics: metrics) { performHeavyOperation() } } ```