Three-layer productization for iOS/macOS test work in Claude Code: a skill that teaches the model to write correct modern tests, a Swift CLI that runs them, and an optional enforcement hook that blocks the old way.
- Skill — Swift Testing, XCTest, XCUITest, performance/accessibility/animation testing, and Instruments .trace analysis.
- CLI (
iostesting) — drive simulators, devices, and.xctestbundles. Build-tool agnostic. - Hook — optional Claude Code
PreToolUsehook that blocksxcodebuild testandxcrun simctl boot/install/launch/...and suggests theiostestingequivalent.
This skill teaches Claude when and how to use each Apple testing framework:
- Swift Testing — default for all new unit and integration tests (
@Test,#expect,@Suite, parameterized tests) - XCTest — performance benchmarks (
measure {},XCTClockMetric,XCTCPUMetric,XCTMemoryMetric,XCTStorageMetric), energy/power measurement (XCTOSSignpostMetric) - XCUITest — UI automation, accessibility audits, animation hitch testing, scroll performance, screenshot capture
- Instruments .trace analysis — export and analyze Time Profiler, System Trace, Allocations, and other trace data without leaving the terminal
The CLI then runs the tests Claude writes. The hook stops Claude from drifting back to raw xcodebuild test.
Here are things you can ask Claude to do with this installed:
- "Write tests for my UserService class"
- "Add parameterized tests for all the edge cases in this parser"
- "Convert these XCTest tests to Swift Testing"
- "Test that this function throws ValidationError.tooShort for empty input"
- "Run the tests on the iPhone 17 Pro simulator"
- "Run just the UserServiceTests in MyAppTests.xctest"
- "Boot the sim, launch the app, take a screenshot in light mode and another in dark mode"
- "Add a performance test that measures CPU and memory for this sort operation"
- "Set up signpost-based measurement for my image processing pipeline"
- "Add scroll deceleration hitch testing for my collection view"
- "Write UI tests for the login flow"
- "Add accessibility audits for contrast and dynamic type on every screen"
- "Set up screenshot capture across light mode, dark mode, and all themes"
- "Analyze this .trace file and tell me why the app is slow"
- "Look at the time-sample data and find the hottest call stacks"
- "What's blocking the main thread in this System Trace?"
AI models frequently:
- Default to XCTest when Swift Testing should be used
- Mix
XCTAssertEqualwith#expectin the same file - Use bare
measure {}instead of specificXCTMetricsubclasses - Invent assertion syntax for
#expect(throws:)orconfirmationfrom memory - Skip parameterized tests in favor of copy-pasted test functions
- Reach for raw
xcodebuild testorxcrun simctlwhen a dedicated CLI exists - Forget about performance, power, and animation testing entirely
- Can't analyze .trace files without manual xctrace export/XML parsing
This package fixes all of that across three reinforcing layers.
npm install -g @ioloro/ios-testingThe skill installs to ~/.claude/plugins/ and activates on test-related requests.
cd cli
swift build -c release
# Copy or symlink .build/release/iostesting to a directory on PATH
# One-time: merge iostesting's permission allowlist into ~/.claude/settings.json
# so Claude Code stops prompting for `xcrun simctl spawn`, `swift build`, etc.
iostesting setupRequires Xcode 14+ and Swift 6 (tested on Swift 6.3 / Xcode 26.4).
iostesting setup is idempotent. It preserves unknown keys + the order of any pre-existing permissions.allow entries, and only appends the curated iostesting list (no MCP entries, no machine-specific paths). Run with --dry-run to preview the diff.
Add to ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "hooks": [{ "command": "/absolute/path/to/ios-testing/hooks/iostesting-guard.sh", "type": "command" }], "matcher": "Bash" }
]
}
}Then restart Claude Code. See hooks/README.md for details.
| File | Covers |
|---|---|
SKILL.md |
Framework selection, critical rules, common-mistakes table, anti-hallucination rules, iostesting handoff |
swift-testing.md |
@Test, @Suite, #expect, #require, parameterized tests, traits, tags, async patterns, confirmation(), exit tests, attachments, custom scoping traits |
xctest.md |
measure {}, all XCTMetric subclasses, manual measurement, signpost-based energy testing, baselines, power profiling |
xcuitest.md |
Element queries, waiting patterns, launch config, scroll/animation performance, hitch metrics, accessibility audits, screenshot capture, multi-variant screenshot testing, real device testing, screenshot extraction and review website generation, page object pattern |
trace-analysis.md |
Instruments .trace file structure, JSON export workflow, all common schemas by template, XML format reference, analysis patterns |
trace2json.py |
Python 3 script that exports .trace files to self-contained JSON — resolves id/ref deduplication, flattens backtraces, caps output size |
The iostesting Swift CLI runs the tests Claude writes. 13 top-level commands, 30 leaves:
config show / get / set / reset # eliminate boilerplate
sim list / boot / shutdown / erase / create / prune
sim media-add / location / button / appearance / open-url
install / uninstall / launch / stop # short-id registry
logs # short-id resolution
screenshot
apps list / prune # the registry
device list / install / launch # via xcrun devicectl
test list / run # NDJSON event stream
licenses
setup # merge permissions into ~/.claude/settings.json
Every command supports --json (or NDJSON for streaming) and --examples (anti-hallucination escape valve). Config is project (./.iostesting/config.json) or global (~/.config/iostesting/config.json), with IOSTESTING_SIM / IOSTESTING_BUNDLE_ID env vars for stateless CI invocation.
2.0.0 ships two backends behind a single CLI. The default (IOSTESTING_BACKEND=simctl) shells out to xcrun simctl / xcrun devicectl and works on any Mac with Xcode 14+. Opt into IOSTESTING_BACKEND=fb for direct linkage against the Meta-licensed FBControlCore / FBSimulatorControl / FBDeviceControl / XCTestBootstrap frameworks (see scripts/fetch-frameworks.sh) — unlocks UI automation (tap, swipe), Swift Testing bundles end-to-end on iOS 26 simulators, and full app lifecycle via direct framework calls. A HybridBackend falls through to simctl for methods not yet wired through FB.
See cli/README.md for the full surface, limitations, and per-command examples.
Optional Claude Code PreToolUse hook (iostesting-guard.sh) that blocks raw xcrun simctl boot/install/launch/... and xcodebuild test invocations and suggests the iostesting equivalent. The skill is advice; the hook is enforcement.
See hooks/README.md for install instructions and the full block list.
- Trace file analysis —
trace2json.pyexports any .trace file to JSON in one command, then Claude reads and analyzes it directly - Multi-variant screenshot pipeline — capture every screen across themes, appearances, and user types with structured naming
- Screenshot review website — automatically generates an HTML gallery grouping screenshots by screen with side-by-side variant comparison
- Real device testing — UDID-based destinations, signing setup, fresh install for NUX/onboarding capture
- System sheet handling — dismiss Sign In with Apple, TCC prompts, and notification banners on real devices
- Accessibility audits — automated contrast, hit region, dynamic type, and text clipping checks (iOS 17+)
- Animation hitch testing —
XCTOSSignpostMetricsub-metrics for scroll deceleration, dragging, navigation transitions - Parameterized tests — tuples,
CaseIterable, cartesian products,zip()patterns - Exit tests — process crash verification (Swift 6.2+)
- Page object pattern — encapsulated screen interactions for maintainable UI tests
- Common-mistakes table + anti-hallucination rule — catches the LLM-classic
#expect(throws: SomeError())/setUp()inside Swift Testing /confirmation { confirm.fulfill() }mixups
- App registry + short IDs —
iostesting appstracks every launched app.iostesting stop ab12kwresolves sim and bundle automatically - Saved config + env vars — set
--simand bundle id once per project or per CI job; never type them again - NDJSON test events —
iostesting test run --jsonstreams per-case pass/fail/duration for agent consumption --exampleseverywhere — every command prints curated snippets when in doubt, no hallucination required- Light/dark appearance toggle + camera-roll injection + location override — the productization touches that make screenshot tests painless
- MIT throughout — third-party notices embedded and printable via
iostesting licenses
- Blocks
xcodebuild test/simctl boot/install/launch/screenshot/spawn-log/spawn-xctestwith iostesting suggestions - Allows
xcodebuild build/build-for-testing/simctl list/simctl runtime/notarytool— only the commands iostesting actually replaces - Handles
sudo/env/bash -c "..."wrappers; only blocks when the tool is in command position
This repo ships under one version (skill + CLI + hook). See CHANGELOG.md for the full history. Current version: 1.2.0.
MIT.