> ## Documentation Index
> Fetch the complete documentation index at: https://docs.swiftaiboilerplate.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing Guide

> Run the full ~598-test suite in one pass, write tests, and measure coverage

The v2.2.0 boilerplate ships with roughly 598 tests that all run in a single pass through `Boilerplate.xctestplan`. One `xcodebuild test` against the `SwiftAIBoilerplatePro` scheme exercises every package test target plus the app's unit and UI suites.

<Info>
  **Complete testing strategy in your project:** [TestingStrategy.md](https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/foundations/TestingStrategy.md)
</Info>

## Test Suite Overview

`Boilerplate.xctestplan` wires up 14 test targets: the 12 package test targets and the app's two targets.

<CardGroup cols={2}>
  <Card title="12 package test targets" icon="cube">
    `CoreTests`, `TestSupportTests`, `NetworkingTests`, `StorageTests`, `AuthTests`, `DesignSystemTests`, `FeatureChatTests`, `FeatureRatingTests`, `FeatureSettingsTests`, `PaymentsTests`, `LocalizationTests`, `AITests`
  </Card>

  <Card title="App test suites" icon="mobile">
    `SwiftAIBoilerplateProTests` (unit + integration) and `SwiftAIBoilerplateProUITests` (end-to-end XCUITest flows)
  </Card>
</CardGroup>

<Note>
  `TestSupport` is the v2.2 test-infrastructure package (it exposes `URLProtocolStub` for network stubbing). It is not one of the 11 reusable product packages — it exists only to support the test targets.
</Note>

**Total: \~598 tests, one test-plan run.**

## Run Tests

<Tabs>
  <Tab title="Xcode">
    ```text theme={null}
    # Run the whole test plan
    ⌘ + U

    # Run a single test — click the diamond next to the test function
    # Run a test class — click the diamond next to the class name
    ```
  </Tab>

  <Tab title="Command Line">
    ```bash theme={null}
    # Whole workspace via the test plan
    xcodebuild test \
      -scheme SwiftAIBoilerplatePro \
      -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.2'

    # Convenience wrapper with a coverage report
    ./scripts/run-tests.sh --coverage --open

    # A single package (runs `swift test` inside Packages/<name>)
    ./scripts/run-tests.sh --package Core --coverage
    ```
  </Tab>
</Tabs>

<Note>
  The repo targets the **iPhone 17 Pro, OS=26.2** simulator (the destination CI uses). `scripts/run-tests.sh` itself drives the app test target on **iPhone 16, OS=18.6** to exercise the pre-iOS-26 `SAIGlass` Material fallback — pass any installed destination by editing the script's `DESTINATION` if your simulators differ.
</Note>

## Coverage

Coverage is measured with `xccov`, not asserted per module. `scripts/run-tests.sh --coverage` writes `coverage-report.txt` and an HTML report, and warns (does not fail) if overall app coverage drops below its local `COVERAGE_THRESHOLD` of 85%.

CI enforces a separate **product-coverage floor of 25%** (`MINIMUM_COVERAGE` in `.github/workflows/ci.yml`). It computes coverage over product targets only — test targets and vendored dependencies (`SnapshotTesting`, `InlineSnapshotTesting`, `OneSignal`) are excluded so they don't deflate the number.

## Writing Tests

The packages use XCTest with protocol-based fakes. The patterns below mirror the suites that ship in the repo.

### Unit Test Example

`ChatViewModel` is constructed with `conversationID`, a `MessageRepository`, and an `LLMClient`. You set `inputText` and call `send()`. Tests inject `FakeMessageRepository` and `FakeLLMClient` (from `FeatureChatTests`).

```swift theme={null}
@MainActor
final class ChatViewModelTests: XCTestCase {
    func testSendAppendsUserMessage() async {
        // Arrange
        let fakeRepo = FakeMessageRepository()
        let fakeLLM = FakeLLMClient(chunks: ["Hello", " ", "world"])
        let viewModel = ChatViewModel(
            conversationID: UUID(),
            messageRepository: fakeRepo,
            llmClient: fakeLLM
        )

        // Act
        viewModel.inputText = "Hello, AI!"
        await viewModel.send()

        // Assert — the user message landed and the input cleared
        let userMessages = viewModel.messages.filter { $0.role == .user }
        XCTAssertEqual(userMessages.count, 1)
        XCTAssertEqual(viewModel.inputText, "")
    }
}
```

### Integration Test Example

`CompositionRootTests` wires the real `CompositionRoot` with test configs and asserts the dependency graph is built.

```swift theme={null}
@MainActor
final class CompositionRootTests: XCTestCase {
    func testGraphIsWired() throws {
        let authConfig = AuthConfig(
            supabaseURL: try XCTUnwrap(URL(string: "https://test.supabase.co")),
            supabaseAnonKey: "test_anon_key"
        )
        let paymentsConfig = PaymentsConfig(
            apiKey: "test_api_key",
            entitlementID: "test_entitlement"
        )

        let composition = try CompositionRoot(
            authConfig: authConfig,
            paymentsConfig: paymentsConfig
        )

        XCTAssertNotNil(composition.httpClient)
        XCTAssertNotNil(composition.sessionManager)
        XCTAssertNotNil(composition.makeChatViewModel(conversationID: UUID()))
    }
}
```

### UI Test Example

UI tests set `UI_TESTING=1` in the launch environment so the app skips the first-launch onboarding/permission prompts that would otherwise block automation. They drive the app by its real accessibility labels.

```swift theme={null}
func testChatFlow() throws {
    let app = XCUIApplication()
    app.launchEnvironment["UI_TESTING"] = "1"
    app.launch()

    app.buttons["Debug: Mock Sign In"].tap()
    app.tabBars.buttons["Chat"].tap()
    app.buttons["New Chat"].firstMatch.tap()

    let input = app.textFields["Type a message..."]
    input.tap()
    input.typeText("Hello, test message")
    app.buttons["Send"].tap()

    XCTAssertTrue(app.staticTexts["Hello, test message"].waitForExistence(timeout: 5))
}
```

## CI/CD Integration

`.github/workflows/ci.yml` runs on every push to `main`/`develop` and on pull requests targeting those branches. Its jobs:

* **Build & Test (iOS 26.2)** — builds and runs the full test plan on iPhone 17 Pro / iOS 26.2 with coverage, then enforces the 25% product-coverage floor.
* **Build & Test (iOS 18.6 fallback path)** — `test-ios18-fallback` rebuilds and runs the suite on iPhone 16 Pro / iOS 18.6 to prove the `SAIGlass` Material fallback still compiles and passes on pre-iOS-26 simulators. No coverage gate — it catches fallback-path regressions only.
* **Template Manifest** — validates `template.manifest.json` via `scripts/validate-template-manifest.sh`.
* **Secret Scan** — Gitleaks over the full history.
* **SwiftLint** — `swiftlint lint --strict`.

### Coverage Reports

* The iOS 26.2 job posts a coverage summary as a PR comment.
* `.github/workflows/coverage-report.yml` runs weekly (Mondays 09:00 UTC) and on demand, producing a detailed report and opening a GitHub issue if any module falls below the 25% threshold.
* `scripts/run-tests.sh --coverage --open` generates a local HTML breakdown.

## Test Organization

```text theme={null}
Boilerplate.xctestplan
├── App targets
│   ├── SwiftAIBoilerplateProTests      # unit + integration (CompositionRoot, proxy, paywall)
│   └── SwiftAIBoilerplateProUITests    # XCUITest end-to-end flows (auth, chat, settings)
│
└── Package test targets (12)
    ├── CoreTests
    ├── TestSupportTests
    ├── NetworkingTests
    ├── StorageTests
    ├── AuthTests
    ├── DesignSystemTests
    ├── FeatureChatTests
    ├── FeatureRatingTests
    ├── FeatureSettingsTests
    ├── PaymentsTests
    ├── LocalizationTests
    └── AITests
```

## Best Practices

<AccordionGroup>
  <Accordion title="What to Test">
    **Do test:**

    * ✅ ViewModels (business logic)
    * ✅ Repositories (data access)
    * ✅ Clients (external services)
    * ✅ Error scenarios
    * ✅ Edge cases

    **Don't test:**

    * ❌ SwiftUI Views (snapshot instead)
    * ❌ Third-party libraries
    * ❌ Framework code
  </Accordion>

  <Accordion title="Test Structure">
    * Use Arrange-Act-Assert pattern
    * One assertion per test (when possible)
    * Clear test names (testSendMessage\_WhenOffline\_ShowsError)
    * Mock external dependencies
    * Test in isolation
  </Accordion>

  <Accordion title="Mock Guidelines">
    * Protocol-based mocking
    * Track call counts
    * Configurable behavior
    * Avoid over-mocking
    * Use real objects when simple
  </Accordion>
</AccordionGroup>

## Coverage Measurement

```bash theme={null}
# Generate the local HTML report and open it
./scripts/run-tests.sh --coverage --open

# Coverage for one package only
./scripts/run-tests.sh --package Core --coverage
```

The local report warns below 85% app coverage; CI gates only the 25% product-coverage floor described under [CI/CD Integration](#cicd-integration).

## View Complete Guide

<Card title="View Complete Testing Guide" icon="book" href="https://github.com/SwiftAIBoilerplatePro/SwiftAIBoilerplatePro-Distribution/blob/main/docs/foundations/TestingStrategy.md">
  Complete guide with examples, patterns, and best practices
</Card>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Tests fail on CI but pass locally">
    * Match the CI destination: iPhone 17 Pro, OS=26.2
    * Confirm Xcode 26.2+ (CI selects Xcode 26.2)
    * Clear derived data, then rebuild
    * Look for timing-sensitive async tests
  </Accordion>

  <Accordion title="Coverage lower than expected">
    * Run with `--coverage`
    * Add tests for untested branches and error scenarios
    * Make sure `async` code is actually awaited in the test
  </Accordion>

  <Accordion title="UI tests flaky or stuck on first launch">
    * Set `app.launchEnvironment["UI_TESTING"] = "1"` so the app skips the first-launch onboarding/permission alert that blocks automation
    * Use `waitForExistence(timeout:)` instead of bare `exists`
    * Drive elements by their accessibility labels (e.g. `"Send"`, `"Type a message..."`)
    * Avoid asserting mid-animation
  </Accordion>
</AccordionGroup>
