Comprehensive Testing Guide - beforetheshoes/Traveling-Snails GitHub Wiki

Comprehensive Testing Guide

Overview

The Traveling Snails project implements a comprehensive fail-fast testing strategy that prevents failing code from reaching production through a multi-layered approach:

Pre-commit hooks (local) โ†’ Security Tests (CI) โ†’ Parallel Test Execution โ†’ Branch Protection

Testing Philosophy

Fail-Fast Strategy

Our testing approach is built on the principle that issues should be caught as early as possible in the development cycle:

  1. Pre-commit hooks catch 90% of issues locally before commit
  2. Security tests run first in CI and block everything if they fail
  3. Parallel test execution provides fast feedback on all test categories
  4. Branch protection prevents merges until all tests pass

Zero Failing Commits

No failing code ever reaches CI/CD because:

  • Pre-commit hooks block commits with security violations
  • SwiftLint catches code quality issues before commit
  • All tests must pass before merge to main branch
  • Direct pushes to main are disabled

Test Categories (87 Total Tests)

๐Ÿ”’ Security Tests (4 tests)

Purpose: Block security violations and prevent data exposure

Location: Traveling Snails Tests/Security Tests/

Key Tests:

  • LoggingSecurityTests - Detects sensitive data in logs
  • CodebaseSecurityAuditTests - Scans for security patterns
  • SecurityAndValidationTests - URL validation, input sanitization
  • SwiftLintIntegrationTests - Validates security rule enforcement

What's Blocked:

  • print() statements (must use Logger.shared)
  • Sensitive data exposure in logging
  • Unsafe error message patterns
  • Hardcoded credentials or API keys

๐Ÿงช Unit Tests (15 tests)

Purpose: Core business logic and model validation

Location: Traveling Snails Tests/Unit Tests/

Coverage:

  • Model tests (Trip, Activity, Lodging, Organization)
  • Helper utilities and extensions
  • Constants and configuration validation
  • Protocol implementation testing
  • Import/export functionality

๐Ÿ”— Integration Tests (8 tests)

Purpose: SwiftData/CloudKit integration and cross-component testing

Location: Traveling Snails Tests/Integration Tests/

Key Areas:

  • SwiftData persistence operations
  • CloudKit synchronization
  • Import/export workflows
  • Network error handling
  • Dependency injection validation

โšก Performance Tests (2 tests)

Purpose: Detect infinite recreation bugs and memory leaks

Location: Traveling Snails Tests/Performance Tests/ and Stress Tests/

Critical Issues Prevented:

  • SwiftData infinite recreation bugs
  • Memory leaks in view models
  • Excessive database queries
  • UI performance degradation

๐Ÿ’พ SwiftData Tests (8 tests)

Purpose: Data persistence, relationships, and anti-patterns

Location: Traveling Snails Tests/SwiftData Tests/

Anti-Patterns Prevented:

// โŒ WRONG - Causes infinite recreation
struct BadView: View {
    let trips: [Trip]  // Parameter passing - BLOCKED by tests
}

// โœ… CORRECT - Enforced by tests
struct GoodView: View {
    @Query private var trips: [Trip]  // Direct query
}

๐ŸŽจ UI Tests (28 tests)

Purpose: Navigation, user flows, and component behavior

Location: Traveling Snails Tests/UI Tests/

Coverage:

  • Navigation patterns and state management
  • Component interactions and lifecycle
  • Error state handling
  • Accessibility compliance
  • Localization validation

โš™๏ธ Settings Tests (7 tests)

Purpose: Configuration, sync, and user preferences

Location: Traveling Snails Tests/Settings Tests/

Areas Covered:

  • User defaults persistence
  • iCloud sync diagnostics
  • App settings validation
  • Data browser functionality

Pre-commit Hooks

Installation

# One-time setup
./Scripts/setup-pre-commit-hooks.sh

What Hooks Do

  1. SwiftLint Security Analysis

    • Scans staged Swift files only (performance optimized)
    • Blocks critical security violations
    • Allows warnings but blocks errors
  2. Commit Message Validation

    • Ensures minimum length requirements
    • Detects security-related commits for extra review

Bypassing Hooks (Emergency Only)

# Only use in emergencies
git commit --no-verify -m "Emergency fix"

Local Testing

Comprehensive Test Runner

# All tests with clean output
./Scripts/run-all-tests.sh

# Specific categories
./Scripts/run-all-tests.sh --security-only
./Scripts/run-all-tests.sh --unit-only
./Scripts/run-all-tests.sh --integration-only
./Scripts/run-all-tests.sh --performance-only

# Quick runs (no dependency resolution)
./Scripts/run-all-tests.sh --quick --unit-only

# Build and test
./Scripts/run-all-tests.sh --no-clean

# Tests only (skip SwiftLint)
./Scripts/run-all-tests.sh --test-only

Test Runner Features

  • Colorized output with clear success/failure indicators
  • Dependency validation before running tests
  • xcbeautify integration for clean, readable output
  • Performance timing for each test category
  • Error aggregation with helpful fix suggestions
  • Build artifact cleaning for consistent test environment

CI/CD Pipeline (GitHub Actions)

Workflow Structure

The comprehensive testing workflow (.github/workflows/comprehensive-tests.yml) implements a three-phase approach:

๐Ÿ”’ Phase 1: Security Validation (Blocking)

  • Security Tests - Must pass for pipeline to continue
  • Enhanced SwiftLint - Detailed security analysis with JSON output
  • Dependency Scanning - Vulnerability detection in Swift packages
  • Secret Detection - Scan for hardcoded credentials

โšก Phase 2: Parallel Test Execution

All jobs run in parallel after security validation passes:

  • Unit Tests - Core functionality validation
  • Integration Tests - SwiftData/CloudKit operations
  • Performance Tests - Infinite recreation prevention
  • SwiftData Tests - Data layer validation
  • Build Validation - Project compilation verification

๐Ÿ›ก๏ธ Phase 3: Quality Gates

  • Test Summary - Requires ALL tests to pass
  • Artifact Collection - Test results for debugging
  • Branch Protection - Blocks merges with failing tests

Workflow Benefits

  • Parallel execution - Faster feedback (3-5 minutes total)
  • Caching - Swift packages and build artifacts cached
  • Timeouts - Jobs don't hang indefinitely
  • Detailed reporting - JSON analysis of SwiftLint results
  • Artifact collection - Test logs available for debugging

Branch Protection

Protection Rules

Main branch is protected with these requirements:

  • โœ… 8 required status checks must pass:

    • Security Tests
    • Unit Tests
    • Integration Tests
    • Performance Tests
    • SwiftData Tests
    • Build Validation
    • Enhanced Linting
    • Test Summary
  • โœ… Pull request workflow - Direct pushes blocked

  • โœ… Up-to-date requirement - Branch must be current

  • โœ… Review requirement - At least 1 approval needed

  • โœ… Stale review dismissal - New commits dismiss old reviews

  • โŒ Force pushes disabled - History preservation

  • โŒ Branch deletion blocked - Protection against accidents

Merge Process

  1. Create feature branch from main
  2. Make changes and commit (pre-commit hooks run)
  3. Push to GitHub (triggers CI/CD workflow)
  4. Create pull request
  5. All 8 status checks must pass
  6. Obtain required review(s)
  7. Merge to main (only after all checks pass)

Test Infrastructure

Test Base Classes

SwiftDataTestBase

Provides isolated in-memory testing for SwiftData operations:

@Suite("My SwiftData Tests")
final class MyTests: SwiftDataTestBase {
    @Test("Test data operations")
    func testDataOperations() async throws {
        // modelContext is automatically available and isolated
        let trip = Trip(name: "Test Trip")
        modelContext.insert(trip)
        try modelContext.save()
        
        #expect(trip.persistentModelID != nil)
    }
}

TestServiceContainer

Provides mock services for dependency injection testing:

@Suite("Service Tests") 
final class ServiceTests: MockServiceTestBase {
    @Test("Test with mock services")
    func testServices() async throws {
        // serviceContainer provides all mock services
        let authService = serviceContainer.authenticationService
        // Test with controlled mock behavior
    }
}

Test Utilities

TestLogHandler

Captures and analyzes log output for security violations:

@Test("Ensure no sensitive data in logs")
func testLoggingSecurity() async throws {
    TestLogHandler.captureOutput {
        // Code that might log sensitive data
        Logger.shared.info("Processing trip: \(trip.name)")
    }
    
    let sensitivePatterns = TestLogHandler.detectSensitiveData()
    #expect(sensitivePatterns.isEmpty, "Found sensitive data in logs")
}

Debugging Test Failures

Local Debugging

  1. Run specific test category to isolate issues:

    ./Scripts/run-all-tests.sh --unit-only
    
  2. Check SwiftLint violations:

    swift run swiftlint lint --config .swiftlint.yml
    
  3. Run tests in Xcode for detailed debugging:

    • Open project in Xcode
    • Select test target
    • Press Cmd+U or use Test Navigator

CI/CD Debugging

  1. Check workflow run details in GitHub Actions
  2. Download artifacts for detailed test logs
  3. Review specific job failures for error details
  4. Compare with local test results to identify environment issues

Common Issues

Pre-commit Hook Failures

  • Security violations: Fix print statements, use Logger.shared
  • SwiftLint errors: Run swift run swiftlint --autocorrect
  • Commit message: Ensure minimum 10 character length

CI/CD Failures

  • Security tests: Fix violations in source code, not test files
  • Build failures: Check for missing dependencies or Xcode version issues
  • Timeout issues: May indicate infinite loops or deadlocks

Performance Considerations

Test Execution Performance

  • Pre-commit hooks: 3-10 seconds (staged files only)
  • Local test suite: 2-5 minutes (full test run)
  • CI/CD pipeline: 3-8 minutes (parallel execution)

Optimization Strategies

  1. Staged file analysis - Pre-commit hooks only check changed files
  2. Parallel execution - Multiple test categories run simultaneously
  3. Build caching - Swift packages and derived data cached
  4. Incremental testing - Only run affected test categories during development

Best Practices

Writing Tests

  1. Use Swift Testing framework (@Test, @Suite, #expect)
  2. Isolate test data with SwiftDataTestBase
  3. Test security implications of new features
  4. Mock external dependencies for reliable testing
  5. Test error conditions and edge cases

Maintaining Tests

  1. Keep tests updated with code changes
  2. Remove obsolete tests when features are removed
  3. Add tests for bug fixes to prevent regression
  4. Review test coverage for new features

Security Testing

  1. Test logging patterns for sensitive data exposure
  2. Validate input sanitization for all user inputs
  3. Test error messages don't expose internal details
  4. Verify authentication flows work correctly

Integration with Development Workflow

Daily Development

  1. Write tests first (TDD approach)
  2. Run relevant test categories during development
  3. Commit frequently - pre-commit hooks catch issues early
  4. Review CI/CD status before requesting reviews

Feature Development

  1. Create feature branch from main
  2. Add tests for new functionality
  3. Run comprehensive test suite before push
  4. Create PR and monitor CI/CD status
  5. Address any test failures before requesting review

Bug Fixes

  1. Write test reproducing bug (if not already covered)
  2. Fix the issue
  3. Verify test passes and bug is resolved
  4. Run full test suite to prevent regression
  5. Create PR with test coverage for the fix

This comprehensive testing strategy ensures code quality, security, and reliability while providing fast feedback to developers and preventing issues from reaching production.