swift-testing
How Swift tests are written in these repos: Swift Testing (`@Test`, `#expect`, `#require`), suites, traits and tags, parameterized tests, async and actor-isolated tests, confirmations, testing `@Observable` models and SwiftData, and migrating from XCTest. TRIGGER — load BEFORE writing, editing or reviewing any Swift test file: a path under a `*Tests/` or `*UITests/` directory, a file ending `Tests.swift`/`Test.swift`, or one that imports `Testing` or `XCTest`. Also when the task says `@Test`, `#expect`, XCTestCase, XCTest migration, test plan, flaky test or "write a test" in a Swift or Xcode project. Plain requests count: "add tests for this model", "fix the failing test", or a feature task whose last step is testing it. Load alongside swift-ios, not instead of it. SKIP when no Swift test code is written or judged.
- License 许可
- MIT
- 最近更新
- 2026-11-11
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- swift-testing
- license
- MIT
- metadata
- { "sources": "Merged from twostraws/Swift-Testing-Agent-Skill (Paul Hudson, MIT), AvdLee/Swift-Testing-Agent-Skill (Antoine van der Lee, MIT), and the testing references of twostraws/Swift-Concurrency-Agent-Skill and AvdLee/Swift-Concurrency-Agent-Skill (MIT)." }
- description
- How Swift tests are written in these repos: Swift Testing (`@Test`, `#expect`, `#require`), suites, traits and tags, parameterized tests, async and actor-isolated tests, confirmations, testing `@Observable` models and SwiftData, and migrating from XCTest. TRIGGER — load BEFORE writing, editing or reviewing any Swift test file: a path under a `*Tests/` or `*UITests/` directory, a file ending `Tests.swift`/`Test.swift`, or one that imports `Testing` or `XCTest`. Also when the task says `@Test`, `#expect`, XCTestCase, XCTest migration, test plan, flaky test or "write a test" in a Swift or Xcode project. Plain requests count: "add tests for this model", "fix the failing test", or a feature task whose last step is testing it. Load alongside swift-ios, not instead of it. SKIP when no Swift test code is written or judged.
02 资源文件 10
- 参考文档evals/evals.json14.5 KB
- 参考文档references/async-and-concurrency.md6.6 KB
- 参考文档references/expectations.md3.6 KB
- 参考文档references/fundamentals.md3.1 KB
- 参考文档references/isolation-and-parallelism.md3.3 KB
- 参考文档references/migrating-from-xctest.md4.5 KB
- 参考文档references/new-features.md3.7 KB
- 参考文档references/parameterized.md2.9 KB
- 参考文档references/testing-app-code.md7.0 KB
- 参考文档references/traits-and-tags.md2.3 KB
03 SKILL.md 正文
Swift Testing
How a Swift test is written. What to test is the repo's testing-policy when it ships one. For
the red-green-refactor loop, use tdd; for the code under test, swift-ios.
Swift Testing ships new APIs several times a year, and most training data and blog posts are XCTest. Your instincts reskin XCTest. The rules below exist to stop that. Trust the installed toolchain over memory and over Apple's docs, which lag behind it.
Before you write
- Read the toolchain:
swift --version,xcodebuild -version, orswift-tools-versioninPackage.swift. Gate anything inreferences/new-features.mdon it. - Check the test target's default actor isolation (
SWIFT_DEFAULT_ACTOR_ISOLATIONin build settings,.defaultIsolation(MainActor.self)inPackage.swift). It decides whether tests already run on the main actor. - See what the target already uses. New tests are Swift Testing. Existing XCTest stays as it is unless the task is a migration.
Your defaults that are wrong here
| You reach for | Do this instead |
|---|---|
class FooTests: XCTestCase, XCTAssert* |
A struct with @Test methods, #expect |
@Suite on every test type |
Nothing. @Suite only to give a display name or attach traits |
setUp() / tearDown() |
init() (async/throws allowed); deinit on a class; a scoping trait for async teardown |
func testUserLogsOut() |
func userLogsOut(); the test prefix means nothing |
#expect(!isLoggedIn) |
#expect(isLoggedIn == false); ! hides the value in the failure |
users.first!, XCTUnwrap |
let first = try #require(users.first) |
#expect(throws: Error.self) |
The specific error or case: #expect(throws: LoginError.locked) |
Copy-pasted tests that differ by one input, a for loop in a test |
@Test(arguments:) |
Task.sleep to wait for work |
await the work; confirmation(); return the Task and await it |
XCTestExpectation + wait(for:) |
await confirmation { confirm in … }, or a continuation |
.serialized to fix a flaky test |
Remove the shared state. .serialized only as a dated transition |
.timeLimit(.seconds(10)) |
.timeLimit(.minutes(1)); minutes is the only unit |
@available on a suite |
@available on each test function |
@MainActor on every test |
Only where the code under test is main-actor isolated |
| Commenting a test out | .disabled("reason", .bug(...)), or withKnownIssue to keep it running |
| Unit-testing a SwiftUI view | Test the @Observable model or service behind it |
Rules
- XCTest stays only for UI automation (
XCUIApplication), performance metrics (XCTMetric,measure) and Objective-C tests. Everything else new is Swift Testing. import Testingonly in test targets, never in app, library or executable targets.- Suites are
structs unless they needdeinitor reference identity. Each test gets a fresh instance, so stored properties set ininit()are per-test state, not shared. #expectis the default.try #requirewhen the rest of the test is meaningless if it fails: preconditions and optional unwraps. Never!inside either macro.- Throwing code:
#expect(throws: SpecificError.case), or capture the returned error and assert on it.#expect(throws: Never.self)for "does not throw". - Parameterize when tests share logic and differ in data. Pair inputs with expected outputs as
an array of tuples, not
zipof two arrays. Two argument collections form a Cartesian product. Expected values are literals, never derived from the input. - Every test runs in parallel and in random order. Shared mutable state (
static var, singletons,UserDefaults.standard, the file system) is the bug. Inject it per test. - Async: make the test
asyncandawaitthe real work. Code that spawns aTaskit never returns can't be tested reliably: make itasyncor return theTask. Wrap callback APIs inwithCheckedContinuation. No sleeps as synchronisation. - Test behaviour through the public or
@testableinternal API. Don't addnonisolatedaccessors or test-only hooks to production code to reach into an actor. - Unit tests never touch the network, the real clock, or persistent storage. Use a protocol
double for
URLSession, an injected clock and an in-memoryModelContainer. - New dependencies (Swift Numerics, swift-concurrency-extras, snapshot libraries) need the user's permission first.
- Mirror the production folder layout in the test target for new tests. Don't reorganise existing tests without asking.
Reviews
When asked to review tests, list findings by file. For each: line, the rule broken, a short before/after. Skip clean files, and end with a prioritised summary. Report real problems only. When asked to write or fix tests, make the change; don't produce a report.
Where to look
| If you're… | Load |
|---|---|
Structuring suites, init/deinit, nested suites, @available |
references/fundamentals.md |
Choosing #expect/#require, testing throws, Issue.record, withKnownIssue |
references/expectations.md |
Turning repeated tests into arguments:, pairing inputs |
references/parameterized.md |
| Disabling, gating, tagging, linking bugs, test plans | references/traits-and-tags.md |
Testing async code, callbacks, actors, confirmation(), cancellation |
references/async-and-concurrency.md |
Fixing a flaky or order-dependent test, deciding on .serialized |
references/isolation-and-parallelism.md |
Testing an @Observable model, a service, SwiftData, or picking XCUITest |
references/testing-app-code.md |
| Converting XCTest | references/migrating-from-xctest.md |
| Using raw identifiers, exit tests, attachments, scoping traits, range confirmations | references/new-features.md |
See also
| Skill | When |
|---|---|
swift-ios |
The code under test: SwiftUI, models, concurrency |
tdd |
Writing the test first, one behaviour at a time |
testing-policy |
What to test in this repo (per repo, when present) |
04 同仓库更多 Skills
to-tickets
Breaks a source into independently-grabbable GitHub tickets. For a code feature the source is the plan (`docs/plans/<feature>.md`, from /to-plan) and every new or changed symbol lands in exactly one ticket, its signatures copied into the ticket body; a non-code feature is cut from the PRD's slices. Cuts tickets for one feature per run. If a code feature has no approved plan, offers /to-plan instead of cutting from prose. In a repo that runs command-centre, also emits `[[task]]` blocks into its config; in a plain treepad repo, emits a Batch Manifest instead so the tickets materialise as stacked worktrees.
to-seams
Records an agreed-but-unlanded cross-repo interface as a seam: the file a consumer's agent gets pasted into its prompt as ground truth, plus the `[[seam]]` block command-centre reads for the retirement pointer. Use when two repos need to build against an interface neither has landed yet — "record a seam for X", "we agreed on this schema, seam it", "add a seam so Y can build against Z's new field". Halts for approval before writing either file.
to-roadmap
Prioritises settled features into a Now / Next / Later kanban board, rendered as a self-contained ./ideas/roadmap.html. Each card is a feature, not a ticket — one card later becomes several tickets via /to-prd -> /to-plan -> /to-tickets. Boards the features in ./docs/approach.md, or the "## Accepted ideas" in ./ideas/CONTEXT.md when there is no approach doc. The approach doc is settled truth: this assigns tiers and reasons about ordering, it never re-litigates what was decided.
to-prd
Turns one feature idea into a PRD built on journeys: a one-line problem, journeys (each ending in an observable state) and delivery slices that say what is usable after each. Interviews, explores the codebase, and saves to docs/prds/<feature>.md. Covers exactly one feature: given a multi-feature source, ask which one. Reads any approach doc or ideate report as priors so it doesn't re-ask what's settled, but needs none. Starts the per-feature spine (/to-prd -> /to-plan -> /to-tickets).
to-plan
Turns one feature's PRD into a skimmable plan in docs/plans/<feature>.md: a short overview, then per phase a glossary, the journeys it satisfies, and its new and modified definitions (types, enums, ports, schema), with the project:<slug> label. No code, branch or PR; the user's review of the file is the gate. Use for "plan this feature", "write the contract", "what are the types". One feature per run. Slash-only.
ticket-tracker
Use when the request is about the *current state* of GitHub issues rather than creating new ones: "where are my tickets", "show the board", "what's in progress", "what's ready to pick up", "move issue N to in-review", "mark N done", "update the ticket status". Live triage board over GitHub issues via the gh CLI, moving each between backlog -> ready -> in-progress -> in-review -> done using status labels. This is the ticket-level board; ./ideas/roadmap.html is the feature-level one. To file new tickets, use /to-tickets, file-issue for a single ad-hoc one, or /sweep for a batch of small fixes.