Skip to main content

ios-testing

Write, run, debug, and audit tests for iOS/macOS/watchOS/visionOS apps. Use whenever the user is writing or reviewing tests, mocking dependencies, testing async/@Observable/@MainActor/Combine/SwiftData/TCA code, writing XCUITest UI tests, snapshot or performance tests, fixing flaky tests, running tests on a simulator or device, or auditing test quality and coverage. Triggers on: test, @Test, #expect, #require, @Suite, XCTest, XCTestCase, XCUITest, unit test, UI test, snapshot test, performance test, mock, stub, spy, URLProtocol, TDD, "write tests", "add tests", flaky test, "tests fail on CI", code coverage, Swift Testing migration — whether or not the user says "testing".

Source facts

Repository
Emasoft/emasoft-complete-ios-app-authoring
Last source activity
June 11, 2026 at 18:06
Detected SKILL.md language
English
Stars
3
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
20 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
ios-testing
description
Write, run, debug, and audit tests for iOS/macOS/watchOS/visionOS apps. Use whenever the user is writing or reviewing tests, mocking dependencies, testing async/@Observable/@MainActor/Combine/SwiftData/TCA code, writing XCUITest UI tests, snapshot or performance tests, fixing flaky tests, running tests on a simulator or device, or auditing test quality and coverage. Triggers on: test, @Test, #expect, #require, @Suite, XCTest, XCTestCase, XCUITest, unit test, UI test, snapshot test, performance test, mock, stub, spy, URLProtocol, TDD, "write tests", "add tests", flaky test, "tests fail on CI", code coverage, Swift Testing migration — whether or not the user says "testing".
# iOS Testing You are an expert in Apple-platform testing. Your job is to produce tests that are **F.I.R.S.T-compliant, production-ready, and PR-shippable** — and to make them actually run and pass before declaring the work done. ## Two iron rules 1. **Do not invent assertion syntax.** If you can't recall the exact shape of `#expect(throws:)`, `try #require`, `confirmation`, `@Test(arguments:)`, `withMainSerialExecutor`, a TCA `TestStore` closure, or a snapshot strategy — read the relevant `references/*.md` file *before* writing it. A hallucinated API costs a full build cycle; a read costs seconds. 2. **Writing the test is half the job.** The test must compile, run, and pass before the change is done. Close the loop (`swift test`, `xcodebuild test`, or a simulator runner) — don't hand back red or unverified tests. **TDD commit discipline (for agent-written code especially).** A failing test is a hard contract you cannot hallucinate past — without a Red test first, an agent generates code that *looks* correct but satisfies no verifiable requirement. So: **every commit must build and run, even mid-Red** — before committing a new failing test, add the minimum stub (empty method, `fatalError()`, placeholder type) so the project compiles; never commit a file referencing a type/method that doesn't yet exist. One commit per passing test, then write the next (red → green → refactor → commit). ## Decision flowchart (read first) ``` ┌──────────────────────────────┐ │ What am I testing? │ └──┬──────────┬──────────┬─────┘ │ │ │ logic / async UI / screens perf / energy / hitches │ │ │ ▼ ▼ ▼ ┌────────────┐ ┌──────────┐ ┌──────────────────────┐ │ Swift │ │ XCUITest │ │ XCTest + XCTMetric │ │ Testing │ │ (part of │ │ Physical device + │ │ (default, │ │ XCTest) │ │ Release scheme │ │ Xcode 16+)│ │ │ │ REQUIRED for real │ └─────┬──────┘ └────┬─────┘ │ numbers. │ │ │ └──────────┬───────────┘ └──────┬──────┴──────────────────┘ ▼ ┌──────────────────────────────────┐ │ It compiles + builds. Now RUN it.│ ├──────────────────────────────────┤ │ logic/unit → swift test or │ │ xcodebuild test │ │ UI bundle → xcodebuild test │ │ performance → physical device, │ │ Release scheme │ └──────────────────────────────────┘ ``` ## Framework selection Default to **Swift Testing** (`import Testing`) for all new unit and integration tests on Xcode 16+. Keep **XCTest** (`import XCTest`) only where Swift Testing genuinely can't go. | What you are testing | Framework | Import | |---|---|---| | Unit / integration / logic / async code | **Swift Testing** | `import Testing` | | Performance benchmarks (CPU, memory, clock, storage) | **XCTest** | `import XCTest` | | Energy / power, animation hitches, scroll/render metrics | **XCTest** (+ XCUITest) | `import XCTest` | | UI automation, element interaction, accessibility audits | **XCUITest** | `import XCTest` | | Light/dark appearance, contrast/visibility | **XCUITest** (launch args + a11y audit) | `import XCTest` | | Snapshot / visual regression | **XCTest or Swift Testing** + swift-snapshot-testing | either | | Exit / crash tests (Swift 6.2+) | **Swift Testing** | `import Testing` | | Objective-C test code, KVO expectations | **XCTest** | `import XCTest` | | `.trace` files from Instruments | **`xctrace` CLI** | n/a | **Both frameworks coexist in one target** — just keep `import Testing` files and `import XCTest` files separate, and never mix assertion styles inside one function. ## F.I.R.S.T principles (the contract for every test) | Principle | Means | Violation signal | |---|---|---| | **F**ast | Unit tests run in milliseconds | real `URLSession`, `Thread.sleep`, disk I/O | | **I**solated | No shared state; order never matters | `static var`, skipped `setUp`, test A breaks B | | **R**epeatable | Same result on any machine, any time | `Date()`, `UUID()`, random data, time zones | | **S**elf-validating | Pass/fail via assertions — never log inspection | `print(result)` with no assertion | | **T**horough | Happy path + error path + edge cases | green-path only, 0% error coverage | **Test distribution target:** ~90% unit (mocked, milliseconds) · ~8% integration (real DB / stubbed network) · ~2% UI (XCUITest). When in doubt, write a unit test. ## Critical rules (apply to every generated/reviewed test) 1. **Swift Testing for new tests** — `@Test`/`@Suite`/`#expect`. XCTest only for the carve-outs above. 2. **Never mix assertion frameworks within a function.** `XCTAssert*` inside a `@Test` function is **silently swallowed** (always passes); `#expect` inside an `XCTestCase` method is **silently ignored** (no-op). These are 🔴 Critical bugs — they make tests lie. 3. **Put the full expression inside `#expect()`** — never pre-evaluate to a `Bool`. `#expect(sut.validate(x))` gives `"sut.validate(x) → false"`; `let ok = sut.validate(x); #expect(ok)` gives a useless `"Expectation failed"`. 4. **Use Swift Testing equivalents**, not XCTest legacy: - `#expect(a == b)` not `XCTAssertEqual` - `try #require(optional)` not `XCTUnwrap` — on its own line; `#require` does **not** nest inside `#expect` - `#expect(throws: SomeError.self) { … }` (a **type** or value-matching closure — never an instance like `SomeError()`) - `confirmation { confirm in … confirm() }` not `XCTestExpectation` + `wait(for:)` — `confirm` is **called** like a function, not `.fulfill()` - `init()` for setup, not `setUp()` / `setUpWithError()` 5. **One concept per test.** If `and` appears in the test name, split it. 6. **Mock every external dependency at a protocol boundary** — never real `URLSession`, `CoreData`, `FileManager`, or `Keychain` in a unit test. 7. **`@MainActor` on the test TYPE when the SUT is `@MainActor`-isolated** — Swift Testing infers isolation from the type, not the method. Missing it is a Swift 6 compile error. 8. **Each Swift Testing test gets a fresh suite instance** — never rely on shared mutable state between tests. 9. **Cover at minimum: success, failure, edge/empty.** Every async method also gets a cancellation test. 10. **Never compare floats for exact equality** — `#expect(abs(result - 0.3) < 0.0001)`. 11. **Set `.timeLimit(.minutes(1))` on any async test** — a leaked continuation or non-terminating `AsyncStream` hangs the whole suite. 12. **Never `sleep` for timing** — use `confirmation`, `await fulfillment(of:)`, `withMainSerialExecutor`, or `Clock`/`TestClock` injection. In UI tests use `waitForExistence`. 13. **Test behavior through the public API.** Never weaken `private` → `public` just to test. Test data deterministic, not randomized. 14. **Use test data factories** (`Item.sample(…)`) not raw initializers; add memory-leak detection to every ViewModel test. ## Severity labels (always lead with one when diagnosing a test issue) When you answer "why does my test do X?" or flag an anti-pattern, lead with an explicit severity so reviewers can triage: | Severity | Meaning | Examples | |:--:|---|---| | 🔴 **Critical** | Test hangs, crashes, or **lies** | `wait(for:)` inside `async` (deadlock); `XCTAssert*` inside `@Test` (swallowed); `#expect` inside `XCTestCase` (no-op); real `URLSession` in a unit test; shared mutable `static var` between tests; test with **no assertions** | | 🟡 **High** | Runs but breaks F.I.R.S.T / false confidence | missing `@MainActor` on `@MainActor` SUT; no `await fulfillment` for async (timeout race); missing `dropFirst()` on `@Published`; no memory-leak check on a ViewModel; happy-path only | | 🟢 **Medium** | Functional but fragile/unmaintainable | raw initializers instead of factories; `and` in test name; over-mocked boundary; >100-line mock; `setUp`/`tearDown` doing unrelated work | Never omit the label — "hmm, worth a look" is not triage. ## Workflows ### Write a new test suite 1. Identify the component's protocol (create one if missing — in a **separate PR** if extraction is non-trivial). 2. Create the mock: protocol conformance with `stubbed*` returns and `*CallCount` / `*Calls` tracking. 3. Pick the test type: `@MainActor @Suite struct …Tests` (Swift Testing) or `@MainActor final class …: XCTestCase`. 4. Arrange: create mock, inject into SUT (prefer a `makeSUT()` factory that also wires leak tracking). 5. Write at minimum: success, error, edge/empty (+ cancellation for async). 6. **Run the suite — all green before committing.** ### Migrate an XCTest suite to Swift Testing Never in the same PR as production changes. One file at a time (≤ ~200 lines/PR). Apply the mechanical mapping (see [references/refactoring.md](references/refactoring.md)), replace `setUp`/`tearDown` with `init`/stored properties, add `@Suite`/`@Test` display names, mark `@MainActor` where the SUT is isolated. **Danger:** third-party assertion helpers (e.g. some `assertSnapshot`, `assertMacroExpansion`) call `XCTAssert` under the hood and are **silently swallowed** inside `@Test` — keep those as `XCTestCase` until the library adds Swift Testing support. Also ensure TCA ≥ 1.12 (earlier `TestStore` failures don't fail `@Test`). ### Audit an existing suite Scan for anti-patterns ([references/anti-patterns.md](references/anti-patterns.md) has a grep cheat-sheet), measure the test pyramid, find untested ViewModels/critical paths, and produce a severity-ranked report. For a full structured audit (coverage-shape map → anti-patterns → completeness reasoning → compound findings → health score), use [references/test-quality-audit.md](references/test-quality-audit.md). For structural bugs that have **no natural unit test** (data-loss via `[N]→[1]` narrowing, an outdated model consumer dropping fields, a layout/wiring invariant that regresses) use [references/audit-recipes.md](references/audit-recipes.md). When a UI test "can't find an element," triage the root cause *before* editing — [references/ui-testing.md](references/ui-testing.md) has the 6-bucket table. ### Run the tests - **Pure logic** → extract into a Swift Package and `swift test` (≈60× faster than launching a simulator); or a **macOS Framework target** linked by the iOS app, so domain/networking tests run natively on the Mac with no simulator ([references/running-and-ci.md](references/running-and-ci.md)). - **App-hosted unit / integration** → `xcodebuild test -scheme … -destination "platform=iOS Simulator,name=iPhone 17,OS=latest"` (pipe through `xcbeautify`). - **UI (XCUITest)** → `xcodebuild test`. - **Performance** → physical device, Release scheme, perf-only test scheme — **never the simulator** (sim CPU/memory/hitch numbers are unreliable; sim only supports the Duration metric). - **CI speed** → `build-for-testing` once, then fan out `test-without-building -xctestrun …` across runners. See [references/running-and-ci.md](references/running-and-ci.md) for the full command set, Test Plans, coverage collection, device/simulator automation, and [references/system-alerts-ios26.md](references/system-alerts-ios26.md) for the iOS 26 SpringBoard permission-prompt problem that `addUIInterruptionMonitor` cannot reach. ## Common mistakes (high-frequency) | Mistake | Why wrong | Use instead | |---|---|---| | `#expect(throws: SomeError())` | wants a **type** or value-matching closure | `#expect(throws: SomeError.self)` or `#expect(throws: SomeError.tooShort) { … }` | | `try #require` nested inside `#expect(…)` | `#require` is its own assertion | `let x = try #require(optional)` then `#expect(x.foo == …)` | | `confirmation { c in c.fulfill() }` | confused with `XCTestExpectation` | the param is called: `confirm()` | | `@Test(arguments: a, b)` for paired inputs | produces a **cartesian product** | `@Test(arguments: zip(a, b))` | | `setUp()` inside a Swift Testing struct | XCTest method in the wrong framework | `init()` / `init() async throws` | | `wait(for:)` inside `func … async` | blocks the thread → **deadlock** | `await fulfillment(of: [exp], timeout: …)` | | `measure { }` inside `@Test` | Swift Testing has no perf primitive | move to `XCTestCase`, `measure(metrics:)` with specific `XCTMetric` | | matching `app.staticTexts["Welcome"]` (localized) | breaks under any non-English locale | `accessibilityIdentifier` + `app.buttons["welcome"]` | | `app.buttons["tab"]` / `app.tabBars.buttons[…]` on iPad | iPad floating bar emits the ID twice → `Multiple matching elements found`; `tabBars` doesn't match the iPad tree | branch on `userInterfaceIdiom` (not width); iPad → `.matching(identifier:).firstMatch` (`ui-testing.md`) | | `.accessibilityIdentifier` on a `VStack`/`List`/card wrapper | the leaf control becomes unreachable or resolves as `StaticText` | put the ID on the interactive leaf (`ui-testing.md`) | | discarding `waitForExistence(timeout:)` return | test proceeds when the element never appeared | `XCTAssertTrue(el.waitForExistence(timeout: 5))` then interact | | running perf/hitch tests on the simulator | sim metrics are meaningless | physical device, Release config | | hardcoded `UIColor.black` / `Color.white` | breaks dark mode + dynamic-type contrast | semantic `UIColor.label`, `.systemBackground`, SwiftUI `.primary` | | auditing a11y only on the first screen | misses regressions on later screens | `performAccessibilityAudit` on **every** screen in the flow | ## Confidence checklist (verify before finalizing) ``` [ ] F.I.R.S.T — no real network, no shared state, no Date()/UUID(), has assertions, covers error paths [ ] Correct framework — Swift Testing for new code; no framework mixing within a function [ ] One concept per test — no "and" in the name [ ] Mocks protocol-based — no concrete deps, no URLSession in a unit test [ ] @MainActor present on the test type when the SUT is @MainActor [ ] Memory-leak detection on every ViewModel test (trackForMemoryLeaks / makeSUT) [ ] Async safety — confirmation/await fulfillment (not wait(for:)), .timeLimit set [ ] Expressions live INSIDE #expect() — never a pre-evaluated Bool [ ] Architecture-correct patterns for MVVM / VIPER / TCA [ ] It actually compiled and ran green ``` ## Reference files | Reference | Read when | |---|---| | [references/swift-testing.md](references/swift-testing.md) | `@Test`, `@Suite`, `#expect`, `#require`, parameterized tests, tags/traits, `confirmation`, known issues, exit tests, attachments, parallel-execution dangers | | [references/xctest-patterns.md](references/xctest-patterns.md) | `XCTestCase` structure, the stub+spy mock pattern, memory-leak detection, `@Published`/Combine testing, scheduler injection, coordinator testing, performance tests | | [references/async-testing.md](references/async-testing.md) | `withMainSerialExecutor`, `confirmation` semantics, `await fulfillment` vs `wait(for:)`, `Clock`/`TestClock`/`ImmediateClock`, `AsyncStream`, cancellation, timeouts |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub