Skip to main content
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.
Complete testing strategy in your project: TestingStrategy.md

Test Suite Overview

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

12 package test targets

CoreTests, TestSupportTests, NetworkingTests, StorageTests, AuthTests, DesignSystemTests, FeatureChatTests, FeatureRatingTests, FeatureSettingsTests, PaymentsTests, LocalizationTests, AITests

App test suites

SwiftAIBoilerplateProTests (unit + integration) and SwiftAIBoilerplateProUITests (end-to-end XCUITest flows)
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.
Total: ~598 tests, one test-plan run.

Run Tests

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.

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).

Integration Test Example

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

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.

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.
  • SwiftLintswiftlint 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

Best Practices

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
  • Use Arrange-Act-Assert pattern
  • One assertion per test (when possible)
  • Clear test names (testSendMessage_WhenOffline_ShowsError)
  • Mock external dependencies
  • Test in isolation
  • Protocol-based mocking
  • Track call counts
  • Configurable behavior
  • Avoid over-mocking
  • Use real objects when simple

Coverage Measurement

The local report warns below 85% app coverage; CI gates only the 25% product-coverage floor described under CI/CD Integration.

View Complete Guide

View Complete Testing Guide

Complete guide with examples, patterns, and best practices

Troubleshooting

  • 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
  • Run with --coverage
  • Add tests for untested branches and error scenarios
  • Make sure async code is actually awaited in the test
  • 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