--- source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/testing.mdc confidence: 0.9 namespace: work last_synced: 2026-04-28 alwaysApply: false --- # Testing Guidelines & Best Practices *This guide covers testing practices and patterns for the DuckDuckGo browser on iOS and macOS platforms.* ## 🚨 MANDATORY: Testing Execution Rules ### NEVER Run Tests Without Permission **NEVER execute any test commands without EXPLICIT user permission or unles user explicitly asked to in their prompt.** #### Required Testing Workflow: 1. Write or modify test code as requested 2. if user did not ask to run tests in their prompt, **STOP** before running any test commands: - `swift test` - `npm test` - `xcodebuild test` - `fastlane test` - Any other test execution commands 3. **ASK** the user: "Should I run the tests?" 4. **WAIT** for explicit permission (e.g., "yes", "run tests", "test it") 5. Only then execute test commands **This rule applies to ALL test execution - unit tests, integration tests, UI tests, performance tests, etc.** --- ## Future Improvements This guide is a living document. Consider these areas for future improvements: - **Tab Extensions Testing**: Expand patterns for testing complex tab extension interactions and lifecycle management - **WebKit Integration Testing**: Add comprehensive patterns for testing WKWebView configurations, user scripts, and content blocking integration - **Privacy Feature Testing**: Develop specialized testing approaches for tracker protection, HTTPS upgrade, and content blocking rule validation - **Cross-Platform Testing**: Create patterns for testing SharedPackages functionality across iOS and macOS with consistent behavior validation - **Fire Button Integration Testing**: Add comprehensive testing patterns for data clearing workflows across all browser components - **Autofill and Credential Testing**: Expand testing approaches for AutofillCredentialProvider, password management, and form filling scenarios - **Sync Testing**: Develop patterns for testing bookmark sync, conflict resolution, and cross-device data consistency - **AI Chat Integration Testing**: Add testing patterns for AI chat functionality, context management, and user interaction flows - **Feature Flag Testing**: Expand MockFeatureFlagger usage patterns and integration testing with real feature configurations ## Unit Tests ### What to Include Unit tests should focus on testing individual components, functions, or classes in isolation. They should be: - **Fast**: Run quickly (< 1 second per test) - **Independent**: Not depend on external systems or other tests - **Deterministic**: Always produce the same result given the same input - **Focused**: Test one specific behavior or functionality ### βœ… When to Write Unit Tests #### Model Logic Testing business logic, data transformations, and model behavior: ```swift func testBookmarkFolderCreation() { let folder = BookmarkFolder(title: "Test Folder") XCTAssertEqual(folder.title, "Test Folder") XCTAssertTrue(folder.children.isEmpty) } ``` #### Algorithms/Parsers Testing parsing logic, URL manipulation, search algorithms: ```swift func testURLSchemeDetection() { let detector = URLSchemeDetector() XCTAssertTrue(detector.isValidURL("https://duckduckgo.com")) XCTAssertFalse(detector.isValidURL("invalid-url")) } ``` #### Utility Functions Testing helper functions, extensions, formatters: ```swift func testDateFormatter() { let formatter = DateFormatter.shortDate let date = Date(timeIntervalSince1970: 1640995200) // 2022-01-01 XCTAssertEqual(formatter.string(from: date), "1/1/22") } ``` #### State Management Testing ViewModels, state transitions, and data flow: ```swift func testViewModelStateTransition() { let viewModel = SearchViewModel() viewModel.performSearch("test query") XCTAssertEqual(viewModel.state, .loading) } ``` ### ❌ What to Avoid #### Simple Property Toggles Testing trivial getters/setters: ```swift // ❌ DON'T test this func testIsEnabledToggle() { feature.isEnabled = true XCTAssertTrue(feature.isEnabled) } ``` #### Complex UI Interactions Use Integration or UI tests instead. #### External Dependencies File system, network calls, databases. #### State/Strategy Pattern Switching These are better suited for integration tests: ```swift // ❌ DON'T test state switching in unit tests func testStateSwitching() { stateMachine.transition(to: .loading) stateMachine.transition(to: .loaded) // This is brittle and doesn't test real behavior } ``` ## Mocks and Test Helpers The DuckDuckGo browser project includes multiple mock categories for testing different components and scenarios: ### Mock Categories #### Unit Tests Mocks For testing individual components in isolation: - **UI mocks**: MockWindow, MockTabViewItemDelegate - **WebView mocks**: WebViewMock, WKSecurityOriginMock - **Storage mocks**: FileStoreMock, UserDefaultsMock - **Feature-specific mocks**: MockBookmarkManager, MockFireproofDomains, MockAIChatPreferencesStorage #### Integration Tests Mocks For testing component interactions and workflows: - Content blocking mocks - Tab navigation mocks - Fire integration mocks - Onboarding flow mocks - System integration mocks #### BSK Tests Mocks For testing BrowserServicesKit functionality: - **Feature flag mocks**: MockFeatureFlagger - **Privacy configuration mocks**: MockPrivacyConfiguration - **Statistics mocks**: MockStatisticsStore - **Variant management mocks**: MockVariantManager - **Network mocks**: MockAPIService ### Mock Usage Examples #### UI Testing with MockWindow ```swift func testViewControllerLifecycle() { autoreleasepool { let mockWindow = MockWindow() let viewController = MyViewController() mockWindow.contentViewController = viewController viewController.viewDidLoad() // Safe to call with MockWindow XCTAssertNotNil(viewController.view) } // Ensures proper cleanup } ``` #### WebKit Testing with WebViewMock ```swift func testGeolocationPermission() { autoreleasepool { let mockWebView = WebViewMock() let origin = WKSecurityOriginMock.new(url: URL(string: "https://example.com")!) let frameInfo = WKFrameInfoMock(webView: mockWebView, securityOrigin: origin, request: URLRequest(url: URL(string: "https://example.com")!), isMainFrame: true) // Test permission handling without actual web content permissionManager.requestGeolocationPermission(for: frameInfo) { granted in XCTAssertTrue(granted) } } } ``` #### Feature Flag Testing with MockFeatureFlagger **⚠️ CRITICAL**: MockFeatureFlagger is **REQUIRED** for feature tests throughout the entire codebase (iOS, macOS, BSK). Tests will crash without it due to DefaultFeatureFlagger assertions that prevent real feature flag usage in test environments. ```swift func testFeatureFlaggedBehavior() { let mockFlagger = MockFeatureFlagger() mockFlagger.enabledFeatureFlags = [.contextualOnboarding] let feature = SomeFeature(featureFlagger: mockFlagger) XCTAssertTrue(feature.isOnboardingEnabled) } // Alternative approach using featuresStub func testWithFeatureStub() { let mockFlagger = MockFeatureFlagger() mockFlagger.featuresStub = [ "contextualOnboarding": true, "duckPlayer": false ] let component = TestComponent(featureFlagger: mockFlagger) XCTAssertTrue(component.hasOnboarding) XCTAssertFalse(component.hasDuckPlayer) } // Testing feature flag injection into Tab func testTabWithFeatureFlags() { autoreleasepool { let mockFlagger = MockFeatureFlagger() mockFlagger.enabledFeatureFlags = [.tabCrashDebugging] let tab = Tab(content: .newtab, featureFlagger: mockFlagger) XCTAssertTrue(tab.canKillWebContentProcess) } } ``` **Why MockFeatureFlagger is Essential**: - **Test Isolation**: Prevents real feature flag configurations from affecting test outcomes - **Crash Prevention**: DefaultFeatureFlagger includes assertions that crash tests if not using MockFeatureFlagger - **Controlled Environment**: Ensures predictable test behavior regardless of remote configuration - **Performance**: Avoids network calls and complex privacy configuration setup ### Mock Guidelines #### When to Use Each Mock Category - **Unit Tests Mocks**: Testing individual classes, methods, and components in isolation - **Integration Tests Mocks**: Testing workflows, component interactions, and system behavior - **BSK Tests Mocks**: Testing shared functionality, feature flags, and cross-platform components #### Memory Management with Mocks - Always use `autoreleasepool {}` for UI and WebKit mocks - Use shared mock instances when testing multiple scenarios - Reset mock state between tests to prevent pollution - Be careful with reactive mocks that use Combine publishers #### Mock Selection Criteria When creating mocks: - **Check existing mocks first** - Use available mocks before creating new ones - **Create what you need** - Don't hesitate to create mocks for single-use scenarios if no suitable mock exists - **Focus on behavior** - Mock the interfaces and protocols your code depends on - **Make them realistic** - Mocks should behave similarly to real implementations ### Avoid "Testing Mocks" (Anti-patterns) **Golden Rule**: "If you can omit mocking, omit mocking" - The more you mock, the more you deviate from the real system, decreasing test confidence. #### Testing Implementation Details Instead of Behavior ```swift // ❌ BAD - Testing that internal methods are called func testUserServiceCallsCorrectMethods() { let mockRepository = MockUserRepository() let userService = UserService(repository: mockRepository) userService.getCurrentUser() XCTAssertTrue(mockRepository.lookupSessionsCalled) // Testing HOW, not WHAT XCTAssertTrue(mockRepository.getLatestSessionCalled) } // βœ… GOOD - Testing the actual outcome func testUserServiceReturnsCurrentUser() { let mockRepository = MockUserRepository() mockRepository.mockUser = User(id: "123", name: "John") let userService = UserService(repository: mockRepository) let user = userService.getCurrentUser() XCTAssertEqual(user?.name, "John") // Testing WHAT happens, not HOW } ``` #### Over-mocking Creates a Fake System ```swift // ❌ BAD - Testing mostly mock interactions func testCompleteWorkflow() { let mockAuth = MockAuthService() let mockAPI = MockAPIService() let mockStorage = MockStorage() let mockLogger = MockLogger() let mockAnalytics = MockAnalytics() // ... testing interactions between mocks, not real behavior } ``` #### Complex Scenarios Need Integration Testing ```swift // ❌ BAD - Complex mocking setup for user flows func testBookmarkSyncFlow() { let mockNetworkMonitor = MockNetworkMonitor() let mockSyncService = MockSyncService() let mockBookmarkStore = MockBookmarkStore() let mockConflictResolver = MockConflictResolver() // ... 50 lines of mock setup for a flow that should be integration tested } // βœ… BETTER - Use Integration Test instead // This complex scenario is better tested as an integration test // with real components, providing higher confidence and less maintenance ``` When complex mocking suggests better alternatives: - If you're setting up many interconnected mocks β†’ Consider an Integration Test with real components - If you're mocking user interaction flows β†’ Consider a UI Test that exercises the actual user journey - If mock setup takes longer than the test logic β†’ You're probably over-mocking ## Memory Management - TestRunHelper The project includes **TestRunHelper** that provides automatic memory management and tracking during testing. ### TestRunHelper Features (Automatically Enabled) - **Autorelease Tracking**: Automatically enabled for all tests, tracks specific classes (WKWebView, NSWindow, NSWindowController, NSViewController, TabBarItemCellView) - **View Hierarchy Tracking**: Monitors NSView creations and validates deallocation with 3-second timeout - **UI Presentation Prevention**: Fails tests that try to present real UI with `fatalError("Unit Tests should not present UI. Use MockWindow if needed.")` - **Test Instance Variables Clearing**: Monitors test case instance variables and ensures they're cleared after test completion - **Shared Resource Management**: Shared WKProcessPool for WebKit tests, temporary directory cleanup ### Memory Issue Detection TestRunHelper will break/assert in these scenarios: - **CI Environment**: `fatalError()` with test failure - **Local Development**: `breakByRaisingSigInt()` with debugging guidance - **Tab Deallocation Check**: Tab.deinit includes comprehensive WebKit object deallocation verification #### Override allowedNonNilVariables for Legitimate Cases ```swift override var allowedNonNilVariables: Set { ["constantData", "sharedManager", "staticConfiguration"] } ``` ### When to Use autoreleasepool {} **Always Required**: ```swift // 1. View Controller lifecycle testing func testViewControllerLifecycle() { autoreleasepool { let window = MockWindow() let viewController = MyViewController() window.contentViewController = viewController // Test lifecycle methods } // Ensures immediate cleanup } // 2. Heavy object creation in loops func testBatchOperations() { for i in 0..<1000 { autoreleasepool { let object = HeavyObject(data: testData[i]) processObject(object) } // Releases object immediately instead of waiting for test end } } // 3. WebKit and content blocking tests func testContentBlockingRules() { autoreleasepool { let rulesManager = MockContentBlockerRulesManager() let webView = WebViewMock() // Test content blocking behavior } // WebKit objects can be memory intensive } // 4. File system and database operations func testDatabaseOperations() { autoreleasepool { let context = managedObjectContext let objects = createTestObjects(count: 100) context.save() } // Ensures Core Data cleanup } ``` ### Memory Management Best Practices - Use `autoreleasepool` when creating more than a few objects in a test - Always wrap view controller tests that involve UI hierarchy - Use for any test involving file I/O, network operations, or large data - Wrap tearDown operations when dealing with complex object hierarchies - Essential for batch operations and performance tests ### Template for Memory-Safe Testing ```swift func testFeature() { autoreleasepool { // 1. Setup mocks and dependencies let mockWindow = MockWindow() let mockManager = MockSomeManager() // 2. Create objects under test let objectUnderTest = MyObject(dependencies: mockManager) // 3. Perform test operations objectUnderTest.performAction() // 4. Assert results XCTAssertEqual(objectUnderTest.state, .expected) // 5. Objects are automatically released at end of autoreleasepool } } ``` ### AutoreleaseTracker **What it is**: AutoreleaseTracker creates tracker objects that are autoreleased alongside specific tracked objects, maintaining malloc stack traces for debugging memory leaks. **What it tracks**: WKWebView, NSWindow, NSWindowController, NSViewController, TabBarItemCellView **How to debug autorelease issues**: 1. Enable MallocStackLogging in Tests scheme β†’ Arguments β†’ Environment Variables 2. Run your test and wait for deallocation timeout 3. Open Memory Browser in Xcode (Debug β†’ Debug Workflow β†’ View Memory) 4. Search for "AutoreleaseTracker" in Memory Browser 5. Analyze the stack trace to see where the autorelease call was made ## Integration Tests ### What to Include Integration tests verify that multiple components work together correctly. They test: - **Component Interactions**: How different modules communicate - **Data Flow**: End-to-end data processing - **System Integration**: External services, APIs, databases ### βœ… When to Write Integration Tests #### Complex Feature Workflows Testing complete user journeys: ```swift func testBookmarkSyncFlow() { // Test bookmark creation -> sync -> retrieval let bookmark = createBookmark() syncManager.sync() let retrievedBookmark = bookmarkStore.fetch(by: bookmark.id) XCTAssertEqual(bookmark.title, retrievedBookmark?.title) } ``` #### State Management Testing complex state transitions in real scenarios. #### Cross-Module Communication Testing how different packages interact. #### Database Operations Testing Core Data models and persistence. ### ❌ Integration Test Anti-patterns - **Debug Information**: Don't include debug prints, use `Logger.tests` instead - **Real UI Dependencies**: Prefer mock windows and views - **Heavy Resource Usage**: Avoid tests that load unnecessary resources - **External Network Calls**: Use mocked services ### Best Practices for Integration Tests ```swift class IntegrationTestCase: XCTestCase { override func setUp() { super.setUp() // Use mock window to avoid UI dependencies let mockWindow = MockWindow() setupTestEnvironment(window: mockWindow) } override func tearDown() { // Ensure all resources are properly released cleanupTestEnvironment() super.tearDown() } func testFeatureIntegration() { // Test real component interaction without UI let coordinator = FeatureCoordinator() let result = coordinator.performAction() XCTAssertNotNil(result) } } ``` ## BSK Tests BSK (BrowserServicesKit) tests are specialized for the shared browser services package. ### Logging in BSK Tests #### Getting BSK Test Logs **From GitHub Actions**: Download the `bsk-xctest-log-stream.log` artifact from failed builds **Local Development**: ```bash # Use real-time log streaming (in separate terminal) log stream --debug --info --predicate 'process == "xctest"' --style syslog & swift test # For Swift Testing framework tests (using @Test macro): swift test --enable-swift-testing # For specific test target with Swift Testing: swift test --filter PersistenceTests --enable-swift-testing ``` > ⚠️ **AI Assistant Note**: These commands are examples for manual execution only. Never run test commands automatically without explicit user permission. > > **Note**: Use `--enable-swift-testing` flag when running tests that use the Swift Testing framework (`@Test` macro) instead of XCTest. This enables better test output and proper test discovery for Swift Testing tests. #### Enabling Private Data in Console App By default, macOS Console app shows `` instead of actual values for privacy. To see private data for debugging: 1. Install the logging plist (located at `SharedPackages/BrowserServicesKit/com.apple.system.logging.plist`): ```bash sudo cp com.apple.system.logging.plist /Library/Preferences/Logging/com.apple.system.logging.plist ``` 2. Restart Console app to apply changes 3. Private data will now be visible in system logs for debugging purposes **Note**: Only use this for debugging - remove the plist when done to restore privacy protection. ## Tab Extensions Testing Tab Extensions are modular components that provide specific functionality to tabs. For comprehensive guidance on Tab Extensions architecture and implementation patterns, see the **Tab Extensions Knowledge Sharing**. ### Key Files to Reference - `TabExtensions.swift` - Extension registration and architecture patterns - `TabExtensionsBuilder.swift` - Extension initialization and dependency injection - `Tab+Navigation.swift` - setupNavigationDelegate for navigation handling ### Basic Tab Extension Testing ```swift class MyTabExtensionTests: XCTestCase { var extensionsBuilder: TestTabExtensionsBuilder! override func setUp() { super.setUp() // Load only the extension you want to test extensionsBuilder = TestTabExtensionsBuilder(load: [MyTabExtension.self]) } func testExtensionFunctionality() { autoreleasepool { let tab = Tab(content: .none, extensionsBuilder: extensionsBuilder) let myExtension = tab.extensions.myExtension XCTAssertNotNil(myExtension) myExtension?.performAction() XCTAssertEqual(myExtension?.publicProperty, "expected_value") } } } ``` ### Advanced Testing with Mocked Dependencies ```swift class AdClickAttributionTabExtensionTests: XCTestCase { func testExtensionWithMockedDependencies() { let extensionsBuilder = TestTabExtensionsBuilder(load: [AdClickAttributionTabExtension.self]) { builder in { args, dependencies in builder.override { AdClickAttributionTabExtension( // Override with mocked dependencies userContentControllerFuture: Future { $0(.success(self.mockUserContentController)) }, dependencies: dependencies.privacyFeatures.contentBlocking ) { _ in (logic: self.mockLogic, detection: self.mockDetection) } } }} autoreleasepool { let tab = Tab(content: .none, extensionsBuilder: extensionsBuilder) // Test extension behavior with mocked dependencies } } } ``` ### Tab Extension Testing Guidelines #### βœ… Do: - Use TestTabExtensionsBuilder for controlled extension loading - Load only required extensions for isolated testing - Test through public protocol interfaces - Use autoreleasepool for memory management - Mock dependencies through override patterns #### ❌ Don't: - Load all extensions unless testing integration - Access private implementation details - Create real UI (use MockWindow) - Ignore proper setup and teardown ## UI Tests For comprehensive UI testing guidelines, patterns, and best practices specifically for the DuckDuckGo macOS browser, see **[UI Testing Guidelines & Best Practices](ui-testing.md)**. The UI testing documentation covers: - **Element Access Patterns**: Accessibility IDs, element variables, extension usage - **Window and Tab Management**: Multi-window operations, navigation modifiers, validation patterns - **Element Interaction**: Timing best practices, middle-click handling, context menus - **Test Server Integration**: Local test server setup and content creation - **Performance Optimizations**: Pasteboard usage, avoiding slow operations - **Build Architecture**: Compatibility requirements across macOS versions - **Advanced Patterns**: Tab extensions, settings testing, bookmark/history workflows ## Snapshot Testing ### Overview The project uses `swift-snapshot-testing` for snapshot testing, which captures and compares outputs to detect regressions. **Available snapshot types**: - **Image snapshots**: Visual UI testing - **JSON snapshots**: Data structure testing - **Inline snapshots**: Text-based output testing ### When to Use Snapshot Testing #### βœ… Excellent for: **Complex Data Structures**: Testing JSON responses, complex model transformations ```swift // Example: Testing complex suggestion results func testSuggestionResults() { let suggestions = suggestionLoader.getSuggestions(for: "duck") assertInlineSnapshot(of: suggestions.encoded(), as: .lines, matches: """ Suggestions: - Search: "duck" (score: 100) - Bookmark: "DuckDuckGo" (https://duckduckgo.com) - History: "Duck typing" (https://en.wikipedia.org/wiki/Duck_typing) """) } ``` **Visual Regression Testing**: UI components, layouts, visual elements **Algorithm Output Validation**: Testing complex calculations, parsing results **Cross-Platform Consistency**: Ensuring consistent output across iOS/macOS ### Image Snapshot Testing Perfect for visual regression testing: ```swift import SnapshotTesting func testButtonAppearance() { for appearanceName: NSAppearance.Name in [.aqua, .darkAqua] { NSApp.appearance = .init(named: appearanceName)! let button = createStyledButton() assertSnapshot(of: button, as: .image(perceptualPrecision: 0.9), named: appearanceName.rawValue) } } ``` **Image snapshot guidelines**: - Test in both light and dark modes - Use `perceptualPrecision: 0.9` for UI components (allows minor anti-aliasing differences) - Use `perceptualPrecision: 1.0` for pixel-perfect requirements - Create consistent test environments (fixed window sizes, scale factors) ### JSON Snapshot Testing Ideal for data structure validation: ```swift func testBookmarkImport() { let importResult = bookmarkImporter.importFromHTML(testFile) assertSnapshot(of: importResult.bookmarks, as: .json, named: "imported_bookmarks") } ``` **JSON snapshot benefits**: - Catches structural changes in data models - Validates complex transformations - Provides clear diffs for debugging - Language-agnostic format ### Inline Snapshot Testing Best for text-based output and algorithm results: ```swift func testSearchSuggestions() { let results = searchEngine.getSuggestions(for: "privacy") assertInlineSnapshot(of: results.encoded(), as: .lines, matches: """ Privacy Settings Privacy Policy Privacy Tools """) } ``` **Inline snapshot advantages**: - Snapshots live in the test file - Easy to review changes in pull requests - No external files to manage - Great for small, predictable outputs ### Snapshot Testing Best Practices #### Guidelines - **Make tests deterministic**: Use fixed dates, sorted collections, consistent ordering - **Test meaningful changes**: Don't snapshot trivial variations - **Use descriptive names**: `named: "dark_mode_large_text"` instead of `named: "test1"` - **Review snapshot changes**: Always review generated snapshots during development - **Keep snapshots small**: Large snapshots are hard to review and maintain #### Memory Management with Snapshots ```swift func testComplexView() { autoreleasepool { let window = SnapshotWindow() let view = ComplexView(data: testData) window.contentView = view assertSnapshot(of: view, as: .image()) } // Ensures immediate cleanup } ``` ### ❌ When NOT to Use Snapshot Testing - **Highly dynamic content**: Real-time data, user-specific content - **Performance testing**: Snapshot testing is about correctness, not speed - **Simple value comparisons**: Use `XCTAssertEqual` for basic assertions - **Flaky systems**: Outputs that vary between runs ## Test Logging and Debugging ### Using Logger.tests for Test Information **NEVER use `print()` in tests. ALWAYS use `Logger.tests` for debug output:** ```swift import os.log βœ… // GOOD: Use Logger.tests for test debugging func testComplexFlow() { Logger.tests.info("Starting complex flow test") Logger.tests.debug("Setting up test data with \(testData.count) items") Logger.tests.debug("DEBUG: requestCount = \(requestCount), currentState = \(service.currentState)") // Perform test operations Logger.tests.log("Test completed successfully") } ❌ // BAD: Using print() statements func testComplexFlow() { print("Starting test") // Never use print() print("DEBUG: requestCount = \(requestCount)") // Use Logger.tests.debug() instead } ``` **Benefits of Logger.tests:** - Structured logging that integrates with Xcode and CI systems - Proper log levels (info, debug, error) - Automatic collection in CI artifacts - Better performance than print() statements ### Comprehensive Failure Logging For CI debugging, provide comprehensive failure logs: ```swift func testDataProcessing() { Logger.tests.info("Testing data processing with \(inputData.count) items") do { let result = try processor.process(inputData) Logger.tests.debug("Processing completed with \(result.count) results") XCTAssertEqual(result.count, expectedCount, "Expected \(expectedCount) results, got \(result.count). Input: \(inputData)") } catch { Logger.tests.error("Processing failed: \(error)") XCTFail("Processing failed with error: \(error)") } } ``` **Best practices for failure logging**: - Include relevant context in assertion messages - Log the actual values that caused failures - Use structured logging for complex data - Include test setup information that might affect results ## Best Practices ### Test Naming #### XCTest Syntax (traditional): ```swift func testBookmarkCreation_WhenValidData_ShouldSucceed() { } func testUserAuthentication_WithInvalidCredentials_ShouldFail() { } ``` #### @Test Syntax (Swift Testing - preferred for new tests): ```swift @Test("Bookmark creation succeeds with valid data") func bookmark_creation_with_valid_data() { } @Test("User authentication fails with invalid credentials") func user_authentication_fails_with_invalid_credentials() { } @Test("External URLs are validated correctly") func external_urls_are_valid() { } ``` #### @Test Naming Guidelines: - Use clear, descriptive text in `@Test("description")` - Function names should be simple and readable (no "test" prefix needed) - Use underscores for better readability in function names - Focus the description on what behavior is being verified ### Test Organization Group related tests using `// MARK:` comments: ```swift // MARK: - Bookmark Creation Tests // MARK: - Bookmark Deletion Tests ``` ### Async Testing #### Basic Async Testing: ```swift func testAsyncOperation() async { let result = await service.fetchData() XCTAssertNotNil(result) } ``` #### Async Testing with Timeouts: ```swift // Using withTimeout for async operations func testAsyncOperationWithTimeout() async throws { let result = try await withTimeout(5.0) { return try await service.performLongOperation() } XCTAssertNotNil(result) } // Custom timeout error func testAsyncOperationWithCustomError() async throws { do { _ = try await withTimeout(1.0, throwing: CustomTimeoutError()) { return try await service.performSlowOperation() } XCTFail("Should have timed out") } catch is CustomTimeoutError { // Expected timeout } } ``` #### Publisher Testing with Timeouts: ```swift // Testing publisher with timeout using extensions func testPublisherWithTimeout() async throws { let subject = PassthroughSubject() // Use timeout extension for publishers let future = subject.timeout(2.0, "Publisher timeout").first().promise() DispatchQueue.main.async { subject.send("test value") } let result = try await future.get() XCTAssertEqual(result, "test value") } // For testing actual async behavior, use proper event-driven patterns: func testPublisherWithAsyncBehavior() { let expectation = expectation(description: "Should receive published value") let subject = PassthroughSubject() let cancellable = subject .sink { value in XCTAssertEqual(value, "test value") expectation.fulfill() } // Trigger the real event that should cause publication service.performActionThatPublishes() // This internally calls subject.send() wait(for: [expectation], timeout: 1.0) cancellable.cancel() } ``` #### Async Timeout Guidelines: - Use `withTimeout()` for Task-based async operations - prevents indefinite hanging - Use publisher `.timeout()` extensions for Combine workflows - handles stream timeouts gracefully - Choose appropriate timeout values: Short for unit tests (1-5s), longer for integration tests (10s+) - Always test timeout scenarios - ensure your code handles timeouts properly - Use descriptive timeout messages - helps with debugging when timeouts occur ## 🚨 CRITICAL: Async Testing Anti-Patterns ### ❌ NEVER Use These Timing Patterns in Tests **NEVER use arbitrary delays in tests - they make tests flaky, slow, and unreliable:** ```swift ❌ // BAD: Arbitrary time delays func testBadPattern() { // NEVER DO THIS DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { expectation.fulfill() } // OR THIS Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in expectation.fulfill() } // OR THIS Thread.sleep(forTimeInterval: 0.5) wait(for: [expectation], timeout: 5.0) } ``` ### βœ… Use Event-Driven Testing Instead **Replace timing delays with event-driven expectations:** #### Pattern 1: Callback-Based Expectations ```swift βœ… // GOOD: Event-driven testing with callbacks class MockService { var onEventTriggered: (() -> Void)? func triggerEvent() { // Do work... onEventTriggered?() // Signal completion } } func testGoodPattern() { let expectation = expectation(description: "Event should be triggered") mockService.onEventTriggered = { expectation.fulfill() } // Trigger the actual event mockService.triggerEvent() wait(for: [expectation], timeout: 1.0) // Short timeout for deterministic events } ``` #### Pattern 2: Publisher-Based Expectations ```swift βœ… // GOOD: Combine publisher testing func testPublisherPattern() { let expectation = expectation(description: "Publisher should emit value") let cancellable = service.statePublisher .compactMap { $0 } .first() .sink { value in XCTAssertEqual(value, .expectedState) expectation.fulfill() } // Trigger the state change service.updateState(.expectedState) wait(for: [expectation], timeout: 1.0) cancellable.cancel() } ``` #### Pattern 3: Predicate-Based Expectations ```swift βœ… // GOOD: Condition-based waiting func testPredicatePattern() { // Trigger the operation service.startOperation() // Wait for specific condition to be true let predicate = NSPredicate { _, _ in service.isOperationComplete } let expectation = XCTNSPredicateExpectation(predicate: predicate, object: nil) wait(for: [expectation], timeout: 2.0) XCTAssertTrue(service.isOperationComplete) } ``` #### Pattern 4: Inverted Expectations for "Should Not Happen" ```swift βœ… // GOOD: Testing that something should NOT happen func testShouldNotHappen() { let expectation = expectation(description: "Should not trigger unwanted behavior") expectation.isInverted = true // Test passes if expectation is NOT fulfilled mockService.onUnwantedEvent = { expectation.fulfill() // This would fail the test } // Trigger some action service.performAction() wait(for: [expectation], timeout: 1.0) // Short timeout for negative tests // Verify expected behavior occurred instead XCTAssertTrue(service.didPerformExpectedBehavior) } ``` ### Why Event-Driven Testing Is Superior 1. **Deterministic**: Tests wait for actual events, not arbitrary time 2. **Fast**: No unnecessary delays - tests complete as soon as events occur 3. **Reliable**: Eliminates race conditions and timing-dependent failures 4. **Maintainable**: Clear relationship between triggers and expectations 5. **Debuggable**: Failures point to actual logic issues, not timing problems ### Migration Strategy When you see these patterns in existing tests: 1. **Identify the real event** the test is waiting for 2. **Add callback/publisher** to the mock or service to signal that event 3. **Replace arbitrary delays** with event-driven expectations 4. **Use shorter timeouts** (1-2s) since events are deterministic **Remember: Good tests are event-driven, not time-driven!** ## ⏰ Time/Date Testing Patterns ### Critical Design Principle **ALWAYS inject time/date dependencies into your classes - NEVER use `Date()` or `Task.sleep()` directly in production code that needs testing.** ### 🚫 **FORBIDDEN: Task.sleep() in Tests** **NEVER use `Task.sleep()` in any test code:** ```swift // ❌ NEVER DO THIS try await Task.sleep(nanoseconds: 100_000_000) // Creates flaky tests try await Task.sleep(for: .seconds(1)) // Unreliable timing try? await Task.sleep(interval: 0.1) // Arbitrary delays // βœ… DO THIS INSTEAD let expectation = expectation(description: "Wait for async operation") someAsyncOperation { expectation.fulfill() } await fulfillment(of: [expectation], timeout: 1.0) ``` **Why `Task.sleep()` is banned:** 1. **Flaky tests** - Real-world timing varies 2. **Slow tests** - Fixed delays waste time 3. **Unreliable** - May not wait long enough or wait too long 4. **Hides race conditions** - Masks real timing issues ### Timeout Guidelines - Keep test timeouts reasonable: **maximum 5 seconds** for most async operations - Use shorter timeouts (1-3 seconds) when possible to catch issues faster - Only use longer timeouts for truly slow operations (network requests, file I/O) ### 🚫 **NEVER Change Tests to Match Wrong Behavior** **When tests fail, fix the code, not the test:** ```swift // ❌ WRONG: Changing test to match broken behavior XCTAssertEqual(result.count, 3, "Should return 3 items") // Changed from 5 to 3 to make test pass // βœ… RIGHT: Fix the actual code issue // Fix the logic to properly return all expected items XCTAssertEqual(result.count, 5, "Should return all 5 items as originally designed") ``` **Tests should verify correct behavior, not accommodate bugs. If a test fails:** 1. **First** - Check if the production code has a bug 2. **Fix the bug** in the production code 3. **Only then** update the test if the expected behavior has legitimately changed **Never adjust tests to hide problems in the implementation.** When a class needs current time or sleep functionality, inject these dependencies through the initializer: ```swift βœ… // GOOD: Injectable time dependencies class MyService { private let dateProvider: () -> Date private let sleeper: Sleeper init(dateProvider: @escaping () -> Date = Date.init, sleeper: Sleeper = .default) { self.dateProvider = dateProvider self.sleeper = sleeper } func performOperation() async throws { let now = dateProvider() // βœ… Testable // ... do work ... try await sleeper.sleep(for: 1.0) // βœ… Testable } } ❌ // BAD: Hard-coded time dependencies class MyService { func performOperation() async throws { let now = Date() // ❌ Not testable // ... do work ... try await Task.sleep(nanoseconds: 1_000_000_000) // ❌ Not testable } } ``` ### Pattern 1: TestClock for Async Sleep Testing **Use `TestClock` for testing code that uses `Task.sleep()` or periodic operations:** ```swift βœ… // GOOD: TestClock pattern for async sleep testing func testPeriodicUpdates() async throws { let clock = TestClock() let sleeper = Sleeper(clock: clock) // Inject the test sleeper let service = MyPeriodicService(sleeper: sleeper, interval: 2.0) let expectation1 = expectation(description: "First update") let expectation2 = expectation(description: "Second update") var updateCount = 0 service.onUpdate = { updateCount += 1 if updateCount == 1 { expectation1.fulfill() } else if updateCount == 2 { expectation2.fulfill() } } // Start the periodic task let task = service.startPeriodicUpdates() // Wait for first update (immediate) await fulfillment(of: [expectation1], timeout: 1.0) XCTAssertEqual(updateCount, 1) // Advance clock by 2 seconds to trigger next update await clock.advance(by: .seconds(2)) await fulfillment(of: [expectation2], timeout: 1.0) XCTAssertEqual(updateCount, 2) task.cancel() } // Production service with injectable sleeper class MyPeriodicService { private let sleeper: Sleeper private let interval: TimeInterval var onUpdate: (() -> Void)? init(sleeper: Sleeper = .default, interval: TimeInterval) { self.sleeper = sleeper self.interval = interval } func startPeriodicUpdates() -> Task { Task.periodic(interval: interval, sleeper: sleeper) { await self.performUpdate() } } private func performUpdate() async { // Do work... onUpdate?() } } ``` #### TestClock Best Practices **ALWAYS use `Task.megaYield(count: N)` after `clock.advance()` to allow async processing:** ```swift βœ… // GOOD: Proper megaYield usage await clock.advance(by: .seconds(5)) await Task.megaYield(count: 5) // Allow async tasks to process ❌ // BAD: Multiple consecutive megaYield calls await clock.advance(by: .seconds(5)) await Task.megaYield() await Task.megaYield() await Task.megaYield() ❌ // BAD: No megaYield after clock advance await clock.advance(by: .seconds(5)) // Missing yield - async tasks may not complete ``` **Why `megaYield` is critical:** - TestClock advancement is synchronous, but triggered async tasks need time to process - `megaYield(count: N)` ensures async operations complete before assertions - Use `count: 5` as a standard (allows multiple yield cycles) ### Pattern 2: MockDateProvider for Date Testing **Use `MockDateProvider` or `TimeTraveller` for testing code that needs current date:** ```swift βœ… // GOOD: MockDateProvider pattern class MockDateProvider { private var date: Date init(date: Date = Date()) { self.date = date } func setNowDate(_ date: Date) { self.date = date } func advanceBy(_ timeInterval: TimeInterval) { date.addTimeInterval(timeInterval) } func getDate() -> Date { date } } func testDateBasedLogic() { let mockDateProvider = MockDateProvider(date: Date(timeIntervalSince1970: 0)) let service = MyService(dateProvider: mockDateProvider.getDate) // Test with specific date let result1 = service.processData() XCTAssertEqual(result1.timestamp, Date(timeIntervalSince1970: 0)) // Advance time and test again mockDateProvider.advanceBy(TimeInterval.days(1)) let result2 = service.processData() XCTAssertEqual(result2.timestamp, Date(timeIntervalSince1970: 86400)) } // Production service with injectable date provider class MyService { private let dateProvider: () -> Date init(dateProvider: @escaping () -> Date = Date.init) { self.dateProvider = dateProvider } func processData() -> DataResult { return DataResult(timestamp: dateProvider(), data: "...") } } ``` ### Pattern 3: Protocol-Based Date Injection **For more complex date/time scenarios, use protocol injection:** ```swift βœ… // GOOD: Protocol-based date injection protocol CurrentDateProviding { var currentDate: Date { get } } extension Date: CurrentDateProviding { public var currentDate: Date { self } } class MockDateProvider: CurrentDateProviding { var currentDate: Date init(currentDate: Date = Date()) { self.currentDate = currentDate } } class MyService { private let dateProvider: CurrentDateProviding init(dateProvider: CurrentDateProviding = Date()) { self.dateProvider = dateProvider } func isExpired(_ item: Item) -> Bool { return item.expiryDate < dateProvider.currentDate } } func testExpiryLogic() { let mockDateProvider = MockDateProvider(currentDate: Date(timeIntervalSince1970: 1000)) let service = MyService(dateProvider: mockDateProvider) let expiredItem = Item(expiryDate: Date(timeIntervalSince1970: 500)) let validItem = Item(expiryDate: Date(timeIntervalSince1970: 1500)) XCTAssertTrue(service.isExpired(expiredItem)) XCTAssertFalse(service.isExpired(validItem)) } ``` ### Pattern 4: Combined TestClock and MockDateProvider **For services that need both current time and sleep capabilities:** ```swift βœ… // GOOD: Combined time and sleep mocking func testServiceWithTimeAndSleep() async throws { let mockDateProvider = MockDateProvider(date: Date(timeIntervalSince1970: 0)) let clock = TestClock() let sleeper = Sleeper(clock: clock) let service = MyTimedService( dateProvider: mockDateProvider.getDate, sleeper: sleeper ) let expectation = expectation(description: "Operation should complete") service.onOperationComplete = { result in // Verify the result includes the correct timestamp XCTAssertEqual(result.startTime, Date(timeIntervalSince1970: 0)) expectation.fulfill() } // Start operation let task = service.startOperation() // Advance mock time (affects dateProvider) mockDateProvider.advanceBy(5.0) // Advance test clock (affects sleeper) await clock.advance(by: .seconds(1)) await fulfillment(of: [expectation], timeout: 1.0) task.cancel() } ``` ### Time Testing Best Practices 1. **Always inject time dependencies** - Never use `Date()` or `Task.sleep()` directly in production code 2. **Use TestClock for async operations** - When testing `Task.sleep()`, `Task.periodic`, or `Sleeper` 3. **Use MockDateProvider for date logic** - When testing date comparisons, timestamps, or date-based decisions 4. **Test time progression** - Use `advance()` methods to test how your code behaves over time 5. **Test boundary conditions** - Test behavior at midnight, month boundaries, leap years, etc. 6. **Keep time control granular** - Advance time by specific amounts rather than arbitrary delays ### Common Time Testing Mistakes ❌ **Don't use real time in tests:** ```swift // BAD: Unreliable and slow func testBadTimePattern() async { service.scheduleTask() try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds XCTAssertTrue(service.taskCompleted) } ``` βœ… **Use mock time instead:** ```swift // GOOD: Fast and deterministic func testGoodTimePattern() async { let clock = TestClock() let service = MyService(sleeper: Sleeper(clock: clock)) let expectation = expectation(description: "Task should complete") service.onTaskComplete = { expectation.fulfill() } service.scheduleTask() await clock.advance(by: .seconds(2)) await fulfillment(of: [expectation], timeout: 1.0) } ``` **Remember: Control time in tests, don't wait for it!** ### Error Testing Test both success and failure cases: ```swift func testNetworkError_ShouldHandleGracefully() { // Test error handling } ``` ### Skipping Tests Use `XCTSkip` to temporarily disable problematic tests: ```swift // XCTest syntax func testFlakySomething() throws { throw XCTSkip("Flaky test - temporarily disabled") } // @Test syntax @Test("Temporarily disabled flaky test") func flaky_behavior() throws { throw XCTSkip("Flaky test - investigating timing issues") } ``` **XCTSkip Guidelines**: - Use `throw XCTSkip("reason")` to temporarily disable flaky or problematic tests - Function must be marked as `throws` to use XCTSkip - Include a clear reason in the skip message for future investigation ## CI/CD Integration Tests are automatically run in GitHub Actions with: - Parallel execution for iOS and macOS - Automatic log collection on failures - JUnit report generation - Crash report collection - Asana integration for failure tracking ## Debugging Failed Tests ### 1. Check Logs Download log artifacts from GitHub Actions ### 2. Reproduce Locally Use command line tools for consistent reproduction: # Run macOS tests xcodebuild test \ -scheme "macOS Browser" \ -configuration "Debug" \ -skipPackagePluginValidation \ -skipMacroValidation \ -allowProvisioningUpdates=NO \ -only-testing:Unit\ Tests ``` ```bash # Run specific macOS Unit Test (e.g., HotspotDetectionServiceTests) xcodebuild test \ -scheme "macOS Browser" \ -configuration "Debug" \ -skipPackagePluginValidation \ -skipMacroValidation \ -only-testing:Unit\ Tests/HotspotDetectionServiceTests \ -allowProvisioningUpdates=NO ``` ### Integration Tests ```bash # Run specific macOS Integration Test (e.g., DownloadsIntegrationTests) xcodebuild test \ -scheme "macOS Browser" \ -configuration "Debug" \ -skipPackagePluginValidation \ -skipMacroValidation \ -only-testing:Integration\ Tests/DownloadsIntegrationTests \ -allowProvisioningUpdates=NO # Run all Integration Tests xcodebuild test \ -scheme "macOS Browser" \ -configuration "Debug" \ -skipPackagePluginValidation \ -skipMacroValidation \ -only-testing:Integration\ Tests \ -allowProvisioningUpdates=NO ``` ### UI Tests ```bash # Run iOS UI tests xcodebuild test \ -scheme "iOS Browser" \ -workspace DuckDuckGo.xcworkspace \ -destination "platform=iOS Simulator,name=iPhone 15 Pro" \ -only-testing:UITests # Run macOS UI tests xcodebuild test \ -scheme "macOS UI Tests" \ -configuration "Review" \ -skipPackagePluginValidation \ -skipMacroValidation \ -allowProvisioningUpdates=NO \ -only-testing:UI\ Tests ``` # Run specific macOS UI test case xcodebuild test \ -scheme "macOS UI Tests" \ -configuration "Review" \ -skipPackagePluginValidation \ -skipMacroValidation \ -allowProvisioningUpdates=NO \ -only-testing:UI\ Tests/DownloadsUITests ``` > ⚠️ **AI Assistant Note**: Never run test commands automatically without explicit user permission. ### 3. Reproduce Flaky Tests Reduce timeouts to increase failure rate locally: ```swift // Temporarily reduce timeouts to catch race conditions let shortTimeout = 0.1 // Instead of 5.0 XCTAssertTrue(element.waitForExistence(timeout: shortTimeout)) ``` ### 4. Run Multiple Times Use Xcode's repeat testing options: - **Test Navigator**: Right-click test β†’ "Run [TestName] Repeatedly..." - **Test Settings**: Set "Run" to repeat 10-100 times - **Without Building**: Hold Option when clicking Run button for "Run Without Building" - **Command Line**: Use `--repeat-count` with xcodebuild ### 5. Memory Debugging Use Memory Browser and allocation tracking: ```bash # Enable malloc stack logging for detailed memory traces export MallocStackLogging=1 # Run tests with memory debugging xcodebuild test -scheme YourScheme ``` **In Xcode**: - **Memory Browser**: Debug β†’ Debug Workflow β†’ View Memory - **Malloc Stack Logging**: Enable in scheme environment variables - **AutoreleaseTracker**: Search for "AutoreleaseTracker" in Memory Browser to find leaked objects - **Stack Traces**: View allocation stack traces for retained objects --- **For questions or improvements to this guide, please contribute to the documentation or reach out to the iOS/macOS team.** ### Recovering UI Automation Mode (MANDATORY when runner fails to initialize) When every UI test fails immediately with errors like: - "The test runner failed to initialize for UI testing. (Underlying Error: Timed out while enabling automation mode.)" Follow these steps in order: 1) Verify and re-grant Privacy permissions - System Settings β†’ Privacy & Security β†’ Accessibility: enable Terminal and Xcode/Xcode-beta - System Settings β†’ Privacy & Security β†’ Automation: under Xcode/Xcode-beta, allow controlling β€œSystem Events” and Finder 2) Refresh the user session - Quit Xcode and Terminal - Log out and back in (preferred) or reboot the machine - After login, open Xcode once to re-establish automation trust prompts 3) Optional: Reset TCC entries (you will need to re-grant prompts) ```bash tccutil reset Accessibility com.apple.dt.Xcode com.apple.dt.Xcode-Beta com.apple.Terminal tccutil reset AppleEvents com.apple.dt.Xcode com.apple.dt.Xcode-Beta com.apple.Terminal ``` 4) Sanity-check with a minimal known-green test - Run a single previously passing UI test/class (e.g., HTTPSUpgradeUITests) before running failing classes Notes: - Restarting `testmanagerd` is restricted by SIP on recent macOS versions and usually not necessary once you refresh the session. - Record environment-related failures in `failing-ui-tests.md` and resume class-by-class once automation is restored. ### Parsing xcresult Failures (MANDATORY) Always extract failures from `.xcresult` to drive fixes. Use these exact steps: 1) Dump legacy JSON to a temp file ```bash xcrun xcresulttool get object --format json --legacy --path \ "/Users/admin/Library/Developer/Xcode/DerivedData/DuckDuckGo-/Logs/Test/Test-macOS UI Tests-YYYY.MM.DD_HH-MM-SS-+ZZZZ.xcresult" \ > /tmp/xc_root.json ``` 2) Pull top-level failure summaries from ActionResult.issues.testFailureSummaries ```bash python3 - << 'PY' import json, re j=json.load(open('/tmp/xc_root.json')) vals = j.get('actions',{}).get('_values') or [] if not vals: raise SystemExit('no actions in xcresult json') act = vals[0] fails = ((act.get('actionResult',{}) .get('issues',{}) .get('testFailureSummaries',{}) .get('_values')) or []) def decode_url(url): if not url: return ('','') m = re.match(r'^file:\/\/(.*?)#.*StartingLineNumber=(\d+)', url) return (m.group(1), m.group(2)) if m else (url,'') for f in fails: name = (f.get('testCaseName') or {}).get('_value','') msg = (f.get('message') or {}).get('_value','') url = (f.get('documentLocationInCreatingWorkspace') or {}).get('url',{}).get('_value','') filePath, line = decode_url(url) print(f"{name}\t{' '.join(msg.split())}\t{filePath}\t{line}") PY ``` Notes: - Use `--legacy`; the non-legacy command is deprecated and returns no JSON here. - If per-test details aren't under the top-level summaries, traverse `actions._values[].actionResult.testsRef` and follow `summaryRef` ids for each leaf test to collect `failureSummaries`. - Never guess failures by console output; always parse `.xcresult`. 3) Update `failing-ui-tests.md` with a flat list of ❌ tests and exact reasons (include `file:line` when available). Then fix tests one by one, marking progress as: - ❓ fixed/unchecked - βœ… validated - ❌ still failing β€” reason