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, AITestsApp 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.Run Tests
- Xcode
- Command Line
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 withxccov, 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 setUI_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-fallbackrebuilds and runs the suite on iPhone 16 Pro / iOS 18.6 to prove theSAIGlassMaterial fallback still compiles and passes on pre-iOS-26 simulators. No coverage gate — it catches fallback-path regressions only. - Template Manifest — validates
template.manifest.jsonviascripts/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.ymlruns 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 --opengenerates a local HTML breakdown.
Test Organization
Best Practices
What to Test
What to Test
Do test:
- ✅ ViewModels (business logic)
- ✅ Repositories (data access)
- ✅ Clients (external services)
- ✅ Error scenarios
- ✅ Edge cases
- ❌ SwiftUI Views (snapshot instead)
- ❌ Third-party libraries
- ❌ Framework code
Test Structure
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
Mock Guidelines
Mock Guidelines
- Protocol-based mocking
- Track call counts
- Configurable behavior
- Avoid over-mocking
- Use real objects when simple
Coverage Measurement
View Complete Guide
View Complete Testing Guide
Complete guide with examples, patterns, and best practices
Troubleshooting
Tests fail on CI but pass locally
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
Coverage lower than expected
Coverage lower than expected
- Run with
--coverage - Add tests for untested branches and error scenarios
- Make sure
asynccode is actually awaited in the test
UI tests flaky or stuck on first launch
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 bareexists - Drive elements by their accessibility labels (e.g.
"Send","Type a message...") - Avoid asserting mid-animation
