
Swift Testing Expert
- 4k installs
- 431 repo stars
- Updated April 22, 2026
- avdlee/swift-testing-agent-skill
A skill that guides developers through writing, migrating, and debugging Swift tests using Swift Testing APIs on Apple platforms and Swift server targets.
About
This skill provides expert guidance for writing and maintaining Swift tests using the Swift Testing framework on Apple platforms and Swift server projects. It covers test structure with suite organization, assertion macros (#expect and #require), traits and tags for metadata and filtering, parameterized tests to eliminate repetitive test methods, and parallel execution with isolation strategies. Developers use it when creating new test suites, migrating from XCTest incrementally, debugging flaky or async tests, and configuring test plans in Xcode. Key workflows include triage of flakiness versus correctness failures, callback-to-async bridging, known-issue handling with withKnownIssue, and tag-based CI filtering. The skill enforces a behavior contract: Swift Testing is preferred for unit and integration tests while XCTest is retained for UI automation, performance metrics, and Objective-C code. Covers #expect as default assertion and #require for prerequisite-dependent test lines, with explicit guidance on throw expectations
- Covers #expect as default assertion and #require for prerequisite-dependent test lines, with explicit guidance on throw
- Provides a routing map to nine reference files covering fundamentals, parameterization, async waiting, traits, isolation
- Enforces parallel-safe defaults and recommends fixing shared state before applying .serialized trait
- Maps common pitfalls to next-best moves: repetitive test methods to parameterized tests, flaky integration tests to depe
- Defines a migration strategy: convert assertions first, then organize suites, then introduce parameterization and traits
Swift Testing Expert by the numbers
- 4,047 all-time installs (skills.sh)
- +132 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #290 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
swift-testing-expert capabilities & compatibility
- Capabilities
- test writing · xctest migration · parameterized test design · async bridging · flakiness debugging · trait and tag configuration · parallel execution guidance · known issue handling
- Use cases
- testing · debugging · documentation
- Platforms
- macOS
- Runs
- Runs locally
- Pricing
- Free
What swift-testing-expert says it does
Keep migration advice incremental: convert assertions first, then organize suites, then introduce parameterization/traits.
Recommend parameterized tests when multiple tests share logic and differ only in input values.
npx skills add https://github.com/avdlee/swift-testing-agent-skill --skill swift-testing-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4k |
|---|---|
| repo stars | ★ 431 |
| Security audit | 3 / 3 scanners passed |
| Last updated | April 22, 2026 |
| Repository | avdlee/swift-testing-agent-skill ↗ |
What it does
Write, review, migrate, and debug Swift tests using modern Swift Testing APIs including macros, traits, parameterized tests, and async patterns.
Who is it for?
iOS, macOS, watchOS, tvOS, and Swift server developers writing or modernizing unit and integration tests with Xcode 16+ and Swift Testing.
Skip if: UI automation tests using XCUIApplication, performance tests using XCTMetric, or Objective-C-only test code - these remain on XCTest.
When should I use this skill?
Writing new Swift tests, converting XCTest assertions, debugging flaky async tests, designing parameterized test cases, or configuring Xcode test-plan tag filters.
What you get
Developers produce parallel-safe, parameterized, well-organized Swift test suites with clear diagnostics and a reliable incremental migration path from XCTest.
- Parallel-safe Swift test suites using #expect and #require
- Parameterized tests replacing repetitive test methods
- Tag-based test plans for CI filtering
By the numbers
- 9 reference files in the routing map covering distinct testing topics
- 8 rules in the agent behavior contract
- 8 common pitfall-to-next-move mappings documented
Files
Swift Testing
Overview
Use this skill to write, review, migrate, and debug Swift tests with modern Swift Testing APIs. Prioritize readable tests, robust parallel execution, clear diagnostics, and incremental migration from XCTest where needed.
Agent behavior contract (follow these rules)
1. Prefer Swift Testing for Swift unit and integration tests, but keep XCTest for UI automation (XCUIApplication), performance metrics (XCTMetric), and Objective-C-only test code. 2. Treat #expect as the default assertion and use #require when subsequent lines depend on a prerequisite value. 3. Default to parallel-safe guidance. If tests are not isolated, first propose fixing shared state before applying .serialized. 4. Prefer traits for behavior and metadata (.enabled, .disabled, .timeLimit, .bug, tags) over naming conventions or ad-hoc comments. 5. Recommend parameterized tests when multiple tests share logic and differ only in input values. 6. Use @available on test functions for OS-gated behavior instead of runtime #available checks inside test bodies; never annotate suite types with @available. 7. Keep migration advice incremental: convert assertions first, then organize suites, then introduce parameterization/traits. 8. Only import Testing in test targets, never in app/library/binary targets.
First 60 seconds (triage template)
- Clarify the goal: new tests, migration, flaky failures, performance, CI filtering, or async waiting.
- Collect minimal facts:
- Xcode/Swift version and platform targets
- Whether tests currently use XCTest, Swift Testing, or both
- Whether failures are deterministic or flaky
- Whether tests access shared resources (database, files, network, global state)
- Branch quickly:
- repetitive tests -> parameterized tests
- noisy or flaky failures -> known issue handling and test isolation
- migration questions -> XCTest mapping and coexistence strategy
- async callback complexity -> continuation/await patterns
Routing map (read the right reference fast)
- Test building blocks and suite organization ->
references/fundamentals.md #expect,#require, and throw expectations ->references/expectations.md- Traits, tags, and Xcode test-plan filtering ->
references/traits-and-tags.md - Parameterized test design and combinatorics ->
references/parameterized-testing.md - Default parallel execution,
.serialized, isolation strategy ->references/parallelization-and-isolation.md - Test speed, determinism, and flakiness prevention ->
references/performance-and-best-practices.md - Async waiting and callback bridging ->
references/async-testing-and-waiting.md - XCTest coexistence and migration workflow ->
references/migration-from-xctest.md - Test navigator/report workflows and diagnostics ->
references/xcode-workflows.md - Index and quick navigation ->
references/_index.md
Common pitfalls -> next best move
- Repetitive
testFooCaseA/testFooCaseB/...methods -> replace with one parameterized@Test(arguments:). - Failing optional preconditions hidden in later assertions ->
try #require(...)then assert on unwrapped value. - Flaky integration tests on shared database -> isolate dependencies or in-memory repositories; use
.serializedonly as a transition step. - Disabled tests that silently rot -> prefer
withKnownIssuefor temporary known failures to preserve signal. - Unclear failure values for complex types -> conform type to
CustomTestStringConvertiblefor focused test diagnostics. - Test-plan include/exclude by names -> use tags and tag-based filters instead.
Verification checklist
- Confirm each test has a single clear behavior and expressive display name when needed.
- Confirm prerequisites use
#requirewhere failure should stop the test. - Confirm repeated logic is parameterized instead of duplicated.
- Confirm tests are parallel-safe or intentionally serialized with rationale.
- Confirm async code is awaited and callback APIs are bridged safely.
- Confirm migration keeps unsupported XCTest-only scenarios on XCTest.
References
references/_index.mdreferences/fundamentals.mdreferences/expectations.mdreferences/traits-and-tags.mdreferences/parameterized-testing.mdreferences/parallelization-and-isolation.mdreferences/performance-and-best-practices.mdreferences/async-testing-and-waiting.mdreferences/migration-from-xctest.mdreferences/xcode-workflows.md
Swift Testing Reference Index
fundamentals.md-@Test, suites, structure, naming, and baseline patternsexpectations.md-#expect,#require, throw validation, and failure readabilitytraits-and-tags.md- traits, tags, bug linking, conditions, and test-plan filteringparameterized-testing.md- single/multi-argument parameterization,zip, and scaling strategyparallelization-and-isolation.md- default parallel execution, random order,.serialized, and isolation patternsperformance-and-best-practices.md- test speed, determinism, flaky-test prevention, and parallel-safe defaultsasync-testing-and-waiting.md- async/await in tests, callback bridging, and event-stream verificationmigration-from-xctest.md- pragmatic XCTest -> Swift Testing migration workflowxcode-workflows.md- navigator/report workflows, insights, and diagnostics qualityREADME.mdresources section - Apple documentation links for defining, organizing, and migrating tests
Async Testing and Waiting
When to use this reference
Use this file when tests involve async/await functions, completion handlers, streams/events, or timing-related flakiness.
Preferred approach
- Use async test functions and
awaitnaturally. - Keep async test code close to production async patterns.
- Prefer structured concurrency patterns over ad-hoc synchronization.
- Prefer confirmations for async event-style tests that are not naturally awaitable.
Async function test example
import Testing
struct APIClient {
func fetchName() async throws -> String { "Antoine" }
}
@Test func fetchNameReturnsValue() async throws {
let client = APIClient()
let value = try await client.fetchName()
#expect(value == "Antoine")
}Callback bridging
- For completion-handler APIs without async overloads, bridge with:
withCheckedContinuationwithCheckedThrowingContinuation- Keep continuation wrappers minimal and test-focused.
Completion-handler to async bridge
import Testing
func legacyLoad(_ completion: @escaping (Result<Int, Error>) -> Void) {
completion(.success(42))
}
@Test func legacyAPI() async throws {
let value = try await withCheckedThrowingContinuation { continuation in
legacyLoad { result in
continuation.resume(with: result)
}
}
#expect(value == 42)
}Confirmations for asynchronous events
- Use confirmations when validating event delivery/count semantics that do not map cleanly to direct
await. - Set expected counts explicitly:
- exact count for strict validation
- lower-bounded range for at-least semantics
- Keep confirmation scope small and ensure confirmations happen before the confirmation block returns.
Confirmation example
import Testing
@Test func eventIsPublishedTwice() async {
await confirmation("Publishes two events", expectedCount: 2) { confirm in
confirm()
confirm()
}
}Event handlers and multi-fire callbacks
- Avoid unsafe mutable shared counters from callback closures in strict concurrency mode.
- Use isolation-safe patterns (actor state, AsyncSequence wrappers, or thread-safe containers).
- Verify callback counts and ordering explicitly when behavior depends on it.
Actor-isolated counting pattern
import Testing
actor EventCounter {
private(set) var count = 0
func increment() { count += 1 }
}
@Test func countEventsSafely() async {
let counter = EventCounter()
await counter.increment()
await counter.increment()
#expect(await counter.count == 2)
}Avoid legacy waiting anti-patterns
- Do not return from test before async callback work completes.
- Avoid sleeping/time-based waits as primary synchronization.
- Replace brittle waiting with awaitable conditions and deterministic synchronization points.
// Avoid this pattern:
// try await Task.sleep(nanoseconds: 500_000_000)
// #expect(flag == true)Actor isolation in tests
- Isolate tests to a global actor (e.g.
@MainActor) only when behavior truly requires it. - Keep non-UI tests off main actor to preserve realistic concurrency behavior.
Main-actor test only when required
import Testing
@MainActor
@Test func uiModelMutation() {
#expect(true)
}Expectations
When to use this reference
Use this file when writing assertions, migrating from XCTAssert*, testing thrown errors, or documenting known failures.
#expect as the default
- Use
#expectfor most assertions. - Pass natural Swift expressions (
==,>,.contains,.isEmpty, etc.). - Rely on captured sub-expression values for rich diagnostics in Xcode.
- Avoid old XCTest assertion families in Swift Testing tests.
Example: expressive assertions
import Testing
@Test func pricingRules() {
let subtotal = 25
let discount = 5
let total = subtotal - discount
#expect(total == 20)
#expect(total > 0)
#expect([10, 20, 30].contains(total))
}#require for prerequisites
- Use
try #require(...)when later assertions depend on this condition. - Treat
#requireas "guard + fail test early". - Use return value to unwrap optionals safely and reduce noisy optional chaining.
- Prefer this pattern over manual optional checks when failure should halt test flow.
Example: optional precondition + unwrapped usage
import Testing
@Test func parsedURLHasHTTPS() throws {
let value = "https://www.avanderlee.com"
let url = try #require(URL(string: value), "URL should parse")
#expect(url.scheme == "https")
}Throwing behavior checks
- For success-path calls to throwing functions, call directly and assert returned value.
- For expected failure, use throw-aware expectations to verify:
- any throw
- specific error type
- specific error case/value
- Avoid verbose hand-written
do/catchunless custom branching is truly needed.
Example: expected throw and no-throw
import Testing
enum BrewError: Error, Equatable {
case missingBeans
}
func brew(_ hasBeans: Bool) throws -> String {
guard hasBeans else { throw BrewError.missingBeans }
return "coffee"
}
@Test func expectedThrows() {
#expect(throws: BrewError.self) {
try brew(false)
}
}
@Test func expectedNoThrow() {
#expect(throws: Never.self) {
try brew(true)
}
}Known issue handling
- Use
withKnownIssuefor temporary expected failures you still want to compile/run. - Prefer
withKnownIssueover blanket disabling when you need ongoing visibility. - Remove known-issue wrappers once failure condition is fixed.
Example: scope only failing section
import Testing
@Test func checkoutFlow() {
#expect(true) // still validated
withKnownIssue("Checkout backend intermittently returns 503", isIntermittent: true) {
Issue.record("Known upstream issue")
}
#expect(2 + 2 == 4) // rest of test still executes
}Readability upgrade
- Conform complex domain types to
CustomTestStringConvertiblefor concise test output. - Keep production
CustomStringConvertibleseparate from test-specific descriptions when needed.
Example: clean diagnostic descriptions
import Testing
struct Receipt: CustomTestStringConvertible {
let id: UUID
let total: Decimal
var testDescription: String {
"Receipt(total: \(total))"
}
}XCTest mapping quick examples
// XCTAssertEqual(total, 20)
#expect(total == 20)
// try XCTUnwrap(user)
let user = try #require(user)
// XCTFail("Unreachable")
Issue.record("Unreachable")Do / Don't
- Do use
#requirewhen later checks depend on a value. - Do keep
withKnownIssuescopes narrow. - Don't use XCTest assertions in Swift Testing tests.
- Don't hide prerequisite failures inside later optional chaining.
Fundamentals
When to use this reference
Use this file when creating new Swift Testing suites or refactoring test structure before deeper topics like traits, parameterization, or migration.
Building blocks
- Import
Testingonly in test targets. - Use
@Testto declare tests explicitly (global function or type method). - Use suites (
struct,actor, orclass) to group related tests. - Prefer
structsuites for value semantics and accidental-state-sharing prevention. - Use
@Suitewhen adding suite-level traits or display names. - Use nested suites to reflect feature grouping and improve discoverability.
Core examples
Global test function
import Testing
@testable import FoodTruck
@Test("Food truck has a valid default name")
func defaultName() {
let truck = FoodTruck()
#expect(truck.name.isEmpty == false)
}Suite with instance tests
import Testing
@testable import FoodTruck
@Suite("Menu tests")
struct MenuTests {
@Test("Returns no duplicates")
func uniqueItems() {
let items = Menu.default.items
#expect(Set(items).count == items.count)
}
}Nested suites for feature grouping
import Testing
@testable import FoodTruck
struct CheckoutTests {
struct Taxes {
@Test func taxIsRoundedToTwoDigits() {
let total = Checkout.total(subtotal: 10.00, taxRate: 0.0825)
#expect(total == 10.83)
}
}
struct Discounts {
@Test func promoCodeAppliesFixedAmount() {
let total = Checkout.total(subtotal: 20.00, discount: .fixed(5))
#expect(total == 15.00)
}
}
}Recommended defaults
- Keep tests small and behavior-focused.
- Prefer descriptive names over boilerplate
test...prefixes. - Use display names where human-readable output helps triage.
- Keep setup local, or centralize in suite init when shared across tests.
- Avoid hidden global mutable state.
- Use
@MainActoronly when code under test requires main-thread isolation. - Use
@availableon test functions when needed for platform/language gating.
Organization guidance
- Group by feature behavior, not by implementation class only.
- Promote shared traits (e.g. tags) to suite level when all tests inherit them.
- Use tags for cross-cutting grouping across files/targets.
- Keep unrelated tests in separate suites to preserve clear ownership.
Suite constraints to enforce
- If a suite has instance test methods, it must have a callable zero-argument initializer (implicit or explicit, sync/async, throwing or non-throwing).
- If initialization requirements cannot be met, convert tests to static/global functions or refactor suite state.
- Suite types (and containing types) must always be available; do not apply
@availableto suite declarations.
Zero-argument initializer requirement example
import Testing
@Suite
struct SessionTests {
let config: URLSessionConfiguration
// Valid: callable with zero args due to default value.
init(config: URLSessionConfiguration = .ephemeral) {
self.config = config
}
@Test func usesEphemeralByDefault() {
#expect(config == .ephemeral)
}
}Invalid availability placement
import Testing
// Do not do this on suite types:
// @available(iOS 18, *)
@Suite
struct PushTests {
@available(iOS 18, *)
@Test func supportsNewPushFormat() {
#expect(true)
}
}Do / Don't
- Do keep each test focused on one behavior.
- Do use display names where they improve failure readability.
- Don't rely on test execution order.
- Don't annotate suites with
@available; annotate test functions instead.
Review checklist
- Test target imports
Testing, app targets do not. - Suite choice (
struct/actor/class) matches setup and teardown needs. - Instance tests have a callable zero-argument init path.
- Availability is applied to test functions, not suite types.
Migration from XCTest
When to use this reference
Use this file for incremental migration of existing XCTest code to Swift Testing while preserving safety and CI signal.
Coexistence strategy
- Swift Testing and XCTest can coexist in the same target.
- Migrate incrementally; do not block migration on full rewrite.
- A single source file can import both
XCTestandTestingduring migration. - Keep XCTest where Swift Testing does not apply:
- UI automation (
XCUIApplication) - performance APIs (
XCTMetric) - Objective-C-only tests
Mixed import file example
import XCTest
import TestingPractical migration order
1. Convert assertions to #expect / #require. 2. Replace test... naming constraints with explicit @Test. 3. Reorganize classes into suites where helpful. 4. Collapse repetitive methods into parameterized tests. 5. Add traits/tags for control and test-plan filtering.
Example conversion: class method -> Swift Testing function
// Before (XCTest)
final class PriceTests: XCTestCase {
func testDiscountedTotal() {
XCTAssertEqual(Price.total(subtotal: 20, discount: 5), 15)
}
}
// After (Swift Testing)
import Testing
@Test func discountedTotal() {
#expect(Price.total(subtotal: 20, discount: 5) == 15)
}Assertion mapping highlights
- Most
XCTAssert*variants ->#expect(...). - Optional unwrap checks ->
try #require(optionalValue). - Early-stop semantics ->
#requireinstead of globalcontinueAfterFailure = false. XCTFail("...")->Issue.record("...").
Table-style quick mappings
// XCTAssertTrue(isEnabled)
#expect(isEnabled)
// XCTAssertNil(error)
#expect(error == nil)
// XCTAssertThrowsError(try run())
#expect(throws: (any Error).self) { try run() }
// try XCTUnwrap(user)
let user = try #require(user)Suite model differences
- XCTest: class +
XCTestCase. - Swift Testing: struct/actor/class suites, explicit attributes, value-semantics-friendly defaults.
- Setup can move from
setUppatterns to suite init when appropriate. - Teardown can move to
deinitwhen using class/actor suites. - XCTest sync tests default to main actor behavior; Swift Testing runs tests on arbitrary tasks unless explicitly isolated (e.g.
@MainActor).
Setup migration example
import Testing
struct SessionTests {
let session: Session
init() {
self.session = Session(environment: .test)
}
@Test func startsDisconnected() {
#expect(session.isConnected == false)
}
}Async migration specifics
- Prefer
awaitdirectly for async APIs. - Convert completion-handler APIs with
withCheckedContinuation/withCheckedThrowingContinuation. - Replace
XCTestExpectationpatterns with confirmations when testing asynchronous event streams.
Expectation-style flow -> confirmation
import Testing
@Test func receivesAtLeastOneEvent() async {
await confirmation("Receives event", expectedCount: 1...) { confirm in
confirm()
}
}Migration hygiene
- Prefer mechanical, reviewable commits.
- Use editor pattern-replace to accelerate common assertion conversions.
- Avoid mixing XCTest assertions in Swift Testing tests (and vice versa).
Common pitfalls
- Migrating all files at once instead of phased migration.
- Keeping
continueAfterFailurepatterns instead of targeted#require. - Marking every migrated test
@MainActorunnecessarily.
Parallelization and Isolation
When to use this reference
Use this file when tests are flaky in CI, have hidden ordering dependencies, or need to scale execution speed safely.
Default execution model
- Swift Testing runs test functions in parallel by default.
- Execution order is randomized to expose hidden test dependencies.
- Parallelization applies to synchronous and asynchronous tests.
Example: hidden dependency exposed by parallel execution
import Testing
enum SharedStore {
static var counter = 0
}
@Test func incrementsCounter() {
SharedStore.counter += 1
#expect(SharedStore.counter >= 1)
}
@Test func expectsFreshCounter() {
// Flaky if tests run in parallel or random order.
#expect(SharedStore.counter == 0)
}Why this matters
- Faster CI feedback and shorter local iteration loops.
- Better detection of shared-state coupling and flaky behavior.
- More realistic stress on concurrency-sensitive code paths.
Isolation strategy first
- Make tests independent by default.
- Avoid shared mutable globals and singleton mutation across tests.
- Isolate state per test invocation (fresh suite instance helps).
- Prefer deterministic test data setup over implicit ordering assumptions.
Better pattern: isolate per test
import Testing
struct CounterStore {
var counter = 0
mutating func increment() { counter += 1 }
}
@Test func isolatedCounter() {
var store = CounterStore()
store.increment()
#expect(store.counter == 1)
}.serialized as a targeted tool
- Apply
.serializedto suites when tests must run one-at-a-time. - Use it as a transitional safety measure during migration from serial XCTest suites.
- Refactor toward parallel-safe tests before normalizing serialization everywhere.
- Serialized suites can still run alongside unrelated suites in parallel.
Transitional serialization example
import Testing
@Suite(.serialized)
struct LegacyDatabaseTests {
@Test func migrationStepA() { #expect(true) }
@Test func migrationStepB() { #expect(true) }
}Shared resource scenarios
- If tests hit a shared DB/file/service, choose one:
- isolate backing state per test
- use in-memory substitutes
- create separate serial test plan for integration path
- Prefer architecture that supports both fast in-memory tests and selective real integration tests.
Do / Don't
- Do fix shared-state coupling before adding broad serialization.
- Do use in-memory fakes for the fast path.
- Don't rely on execution order.
- Don't mutate singletons across tests without reset/isolation.
Parameterized Testing
When to use this reference
Use this file when you have repeated tests with identical logic and only input changes.
When to parameterize
- Use one parameterized test when behavior is identical and only input changes.
- Replace copy-pasted tests and in-test
forloops with@Test(arguments: ...). - Keep one responsibility per parameterized test to preserve clarity.
Before -> after
// Before: multiple near-duplicate tests.
// @Test func freeFeatureA() { ... }
// @Test func freeFeatureB() { ... }
import Testing
enum Feature: CaseIterable {
case recording, darkMode, networkMonitor
var isPremium: Bool { self == .networkMonitor }
}
@Test("Free features are not premium", arguments: [Feature.recording, .darkMode])
func freeFeatures(_ feature: Feature) {
#expect(feature.isPremium == false)
}Single input collection
- Pass any sendable collection (arrays, ranges, dictionaries, etc.) as arguments.
- Each argument becomes its own independent test case with separate diagnostics.
- Individual failing arguments can be rerun without rerunning all inputs.
Range-based arguments example
import Testing
func isValidAge(_ value: Int) -> Bool { (18...120).contains(value) }
@Test(arguments: 18...21)
func validAges(_ age: Int) {
#expect(isValidAge(age))
}Multiple inputs
- Swift Testing supports up to two argument collections directly.
- Two collections generate all combinations (cartesian product).
- Control explosion by:
- reducing argument sets
- splitting tests by concern
- pairing related values via
zip(...)
Cartesian product example
import Testing
enum Region { case eu, us }
enum Plan { case free, pro }
func canUseVATInvoice(region: Region, plan: Plan) -> Bool {
region == .eu && plan == .pro
}
@Test(arguments: [Region.eu, .us], [Plan.free, .pro])
func vatInvoiceAccess(region: Region, plan: Plan) {
let allowed = canUseVATInvoice(region: region, plan: plan)
#expect((region == .eu && plan == .pro) == allowed)
}zip for paired scenarios
- Use
zipwhen input A must pair with a corresponding input B. - Prefer
zipover full combinations when you need aligned tuples only. - Keep tuples readable and intentional.
zip example
import Testing
enum Tier { case basic, premium }
func freeTries(for tier: Tier) -> Int { tier == .basic ? 3 : 10 }
@Test(arguments: zip([Tier.basic, .premium], [3, 10]))
func freeTryLimits(_ tier: Tier, expected: Int) {
#expect(freeTries(for: tier) == expected)
}zip pitfalls to avoid
Silent truncation: zip stops at the shorter collection. If the two arrays differ in length, the extra elements are silently dropped — no compiler error, no test failure, just missing coverage.
// ❌ Silent gap: the fifth input is never tested
@Test(arguments: zip(
[Status.active, .inactive, .pending, .banned, .suspended],
["Active", "Inactive", "Pending", "Banned"] // one short
))
func statusLabel(_ status: Status, expected: String) {
#expect(label(for: status) == expected)
}Case-order fragility with `CaseIterable`: pairing two allCases arrays with zip breaks silently if enum cases are ever reordered (e.g., by alphabetizing).
// ❌ Fragile: reordering either enum misaligns all pairs
enum Ingredient: CaseIterable { case rice, potato, egg }
enum Dish: CaseIterable { case onigiri, fries, omelette }
@Test(arguments: zip(Ingredient.allCases, Dish.allCases))
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}Prefer explicit array literals or one of the alternatives below.
Paired input alternatives
When inputs and expected outputs must be paired, prefer these over zip to avoid the silent-truncation and case-ordering problems.
Array of tuples (recommended)
Pairs are co-located and impossible to misalign. Adding a new case forces a matching output to be written at the same time.
import Testing
@Test(arguments: [
(Ingredient.rice, Dish.onigiri),
(.potato, .fries),
(.egg, .omelette)
])
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}Dictionary arguments
Expresses a clear mapping; each entry is self-documenting. Requires Hashable keys.
import Testing
@Test(arguments: [
Ingredient.rice: Dish.onigiri,
.potato: .fries,
.egg: .omelette
])
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}Fixed-size zip with InlineArray (Swift 6.2+)
A custom zip overload for InlineArray enforces equal-length arrays at compile time via a generic length parameter. This is not part of the standard library — you must define the helper yourself.
import Testing
// Custom helper: `zip` for two `InlineArray` values of the same length.
func zip<let N: Int, A, B>(
_ a: InlineArray<N, A>,
_ b: InlineArray<N, B>
) -> Zip2Sequence<[A], [B]> {
zip(Array(a), Array(b))
}
// ✅ Compile error if lengths differ — enforced at compile time
@Test(arguments: zip(
InlineArray<2, Ingredient>(.rice, .potato),
InlineArray<2, Dish>(.onigiri, .curry)
))
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}Naming and output quality
- Use meaningful parameter labels and display names.
- Ensure argument types are readable in output; provide custom test description if noisy.
- Keep argument lists easy to scan (multi-line formatting is recommended).
When CaseIterable.allCases is appropriate
Using allCases as arguments is a valid pattern for property-based tests — tests that verify a universal property holds for every member of a type. The key distinction: the expected result is derived from the property being tested, not from a hard-coded mapping.
import Testing
// ✅ Valid: verifying a mathematical property holds for all orientations.
@Test(
"Rotating clockwise four times returns to the original orientation",
arguments: Orientation.allCases
)
func fullRotation(orientation: Orientation) {
#expect(
orientation
.rotated(.clockwise)
.rotated(.clockwise)
.rotated(.clockwise)
.rotated(.clockwise)
== orientation
)
}Avoid allCases when you need concrete, case-specific expected values — use explicit arrays or tuples instead.
Common pitfalls
- Derived expected values masking bugs: when the expected value is derived from the same input expression as the system under test, both sides shift together and bugs pass silently. Use concrete literals in
#expectfor case-specific expectations.
// ❌ Masking: if format(day) returns "monday" instead of "Monday",
// this test still passes because rawValue has the same casing bug.
@Test(arguments: Day.allCases)
func dayLabel(day: Day) {
#expect(format(day) == day.rawValue)
}
// ✅ Concrete: each expectation is an independent data point.
@Test(arguments: [
(Day.monday, "Monday"),
(.friday, "Friday")
])
func dayLabel(day: Day, expected: String) {
#expect(format(day) == expected)
}- Control flow in test bodies:
if/switchinside a parameterized test body mirrors implementation logic. Tests that branch the same way as production code verify themselves rather than the behavior independently.
// ❌ Mirrors implementation — not independent verification.
@Test(arguments: Day.allCases)
func greeting(day: Day) {
if day == .friday {
#expect(greet(day) == "TGIF!")
} else {
#expect(greet(day) == "Hello, \(day)!")
}
}
// ✅ Separate the special case into its own test.
@Test func fridayGreeting() {
#expect(greet(.friday) == "TGIF!")
}
@Test(arguments: [Day.monday, .tuesday, .wednesday, .thursday, .saturday, .sunday])
func standardGreeting(day: Day) {
#expect(greet(day) == "Hello, \(day)!")
}- Using in-test `for` loops instead of parameterized arguments (worse diagnostics).
- Passing huge argument sets that explode combinations and slow CI.
- Mixing multiple concerns into one parameterized function.
- Extracting argument arrays into separate properties or extensions: this hides what the test covers and forces readers to jump between definitions. Keep arguments inline unless the list is genuinely reused across multiple test functions.
Review checklist
- Repetitive tests are consolidated into one parameterized test.
- Arguments reflect domain vocabulary and produce readable failures.
- Paired inputs are modeled as arrays of tuples or dictionaries; use
zipwith equal-length explicit arrays only when the inputs must remain as separate collections. - Paired inputs use tuples or dictionaries rather than
zip(allCases, allCases). #expectuses concrete literal expectations, not values derived from the input itself.- No
if/switchbranching inside parameterized test bodies. CaseIterable.allCasesis only used for property-based assertions, not example-based mappings.
Performance and Best Practices
When to use this reference
Use this file when test runs are slow, flaky, or not scaling in CI, and when you need practical patterns for fast, deterministic Swift Testing suites.
Core principles
- Prefer deterministic tests over timing-sensitive tests.
- Prefer synchronous verification when asynchronous waiting is not required.
- Keep tests independent so parallel execution remains safe and effective.
- Treat
.serializedas a temporary compromise, not a default architecture.
1) Keep tests synchronous where possible
Synchronous tests generally run faster and are easier to reason about.
import Testing
struct PriceCalculator {
static func total(_ subtotal: Int, discount: Int) -> Int { subtotal - discount }
}
@Test func totalCalculation() {
#expect(PriceCalculator.total(100, discount: 20) == 80)
}Avoid introducing async or sleeps for purely synchronous logic.
// Avoid:
// @Test func totalCalculation() async {
// try await Task.sleep(nanoseconds: 100_000_000)
// #expect(...)
// }2) Avoid unnecessary main-actor isolation
@MainActor can reduce useful parallelization and should be used only when code truly needs main-thread isolation.
import Testing
// Good: non-UI logic stays non-main-actor.
@Test func parserIsStable() {
#expect("A,B,C".split(separator: ",").count == 3)
}
// Use @MainActor only for UI/main-thread sensitive code.
@MainActor
@Test func viewModelMutation() {
#expect(true)
}3) Remove shared mutable state
Shared mutable state is a major source of flakiness and parallel failures.
import Testing
enum Globals {
static var token: String?
}
// Flaky pattern:
@Test func writeToken() {
Globals.token = "abc"
#expect(Globals.token == "abc")
}
@Test func expectsNoToken() {
#expect(Globals.token == nil)
}Better: create isolated state per test.
import Testing
struct SessionState {
var token: String?
}
@Test func isolatedTokenState() {
var state = SessionState()
state.token = "abc"
#expect(state.token == "abc")
}4) Prefer in-memory dependencies for the fast path
Use fakes/in-memory repositories for high-volume test runs; reserve real integration dependencies for dedicated plans.
import Testing
protocol CacheStore {
func put(key: String, value: String)
func get(key: String) -> String?
}
final class InMemoryCacheStore: CacheStore {
private var values: [String: String] = [:]
func put(key: String, value: String) { values[key] = value }
func get(key: String) -> String? { values[key] }
}
@Test func cacheRoundTrip() {
let cache = InMemoryCacheStore()
cache.put(key: "user", value: "42")
#expect(cache.get(key: "user") == "42")
}5) Use parameterized tests to reduce overhead and improve diagnostics
import Testing
func isValidPort(_ value: Int) -> Bool { (1...65535).contains(value) }
@Test(arguments: [1, 80, 443, 65535])
func validPorts(_ port: Int) {
#expect(isValidPort(port))
}This reduces duplicated setup code and gives argument-level failure visibility.
6) Keep setup cheap and scoped
- Build expensive fixtures only when needed.
- Prefer per-suite immutable setup for shared readonly data.
- Avoid network/file-system setup in unit tests unless behavior depends on it.
import Testing
struct CurrencyTests {
let rates: [String: Double]
init() {
rates = ["USD": 1.0, "EUR": 0.92]
}
@Test func hasEURRate() {
#expect(rates["EUR"] != nil)
}
}7) Use .serialized narrowly
import Testing
@Suite(.serialized)
struct TemporarySerialDBTests {
@Test func migrationA() async throws { #expect(true) }
@Test func migrationB() async throws { #expect(true) }
}Add TODO context and remove once dependencies are isolated.
8) Flakiness reduction checklist
- No reliance on execution order.
- No shared mutable globals/singletons without reset.
- No arbitrary sleeps as synchronization.
- No hidden external dependencies in unit tests.
- Deterministic fixtures and stable clocks/random sources.
- Explicit known-issue wrappers for temporary failures.
Quick do / don't
- Do optimize for determinism first, then speed.
- Do keep most tests parallel-safe and dependency-light.
- Don't treat test slowness as only a hardware problem.
- Don't move everything to
@MainActoror.serializedto silence flakiness.
Traits and Tags
When to use this reference
Use this file when controlling test execution behavior, linking bug context, and organizing large test suites for targeted runs and CI filtering.
Trait categories
- Informational: display names, bug links, tags.
- Conditional:
.enabled(if:),.disabled(...), availability attributes. - Behavioral:
.timeLimit(...),.serialized.
Basic trait examples
import Testing
@Test("Uploads complete quickly", .timeLimit(.seconds(10)))
func uploadWithinTimeLimit() async throws {
#expect(true)
}
@Test(.disabled("Flaky on CI while investigating issue"), .bug("https://example.com/issues/12"))
func temporaryDisabledTest() {
#expect(true)
}Conditions and disabling
- Use
.enabled(if:)or.disabled(if:)for runtime-evaluated environments. - Use
.disabled("reason")instead of commenting tests out. - Include actionable reason text in disabled traits for CI/test reports.
- Add
.bug(...)to link issue trackers and aid future cleanup.
Runtime condition example
import Testing
enum Runtime {
static let isCI = ProcessInfo.processInfo.environment["CI"] == "true"
}
@Test(.enabled(if: Runtime.isCI))
func ciOnlySmokeTest() {
#expect(true)
}Availability
- Use
@availableon tests when entire behavior is OS-gated. - Prefer
@availableover inline runtime checks for clearer reporting semantics.
import Testing
@available(iOS 18, *)
@Test func modernPushPayload() {
#expect(true)
}Tags
- Declare custom tags and apply to tests/suites for cross-suite grouping.
- Use tags for test-plan include/exclude, navigator filtering, and failure analytics.
- Treat tags as cross-cutting metadata, not a replacement for suite structure.
- Use meaningful domain labels (e.g.
networking,regression,spicy) over vague terms.
Defining and applying tags
import Testing
extension Tag {
@Tag static var networking: Self
@Tag static var regression: Self
}
@Suite(.tags(.networking))
struct APITests {
@Test func fetchUser() async throws {
#expect(true)
}
}
struct CheckoutTests {
@Test(.tags(.regression))
func orderTotal() {
#expect(3 * 3 == 9)
}
}Inheritance and scope
- Traits and tags on suites cascade to contained tests.
- Apply at suite level when broadly true; apply per test when specific.
- Keep trait intent explicit to avoid accidental broad behavior changes.
Do / Don't
- Do put shared tags at suite level for consistency.
- Do attach bug links for temporary disables or known failures.
- Don't use tags as a replacement for meaningful suite grouping.
- Don't overuse
.serializedas a blanket reliability fix.
Review checklist
- Every disabled test has a reason (and ideally a bug link).
- Tags reflect domain concerns and are reused consistently.
- Availability and condition traits are applied to the smallest correct scope.
Xcode Workflows
When to use this reference
Use this file when debugging failures quickly in Xcode, configuring focused test plans, and extracting insights from large test reports.
Test Navigator usage
- Run tests at function, suite, tag, and argument level.
- In parameterized tests, rerun only failing arguments for fast iteration.
- Use "Group by Tag" to inspect cross-suite behavior quickly.
Example flow
1. Run suite. 2. Open failing parameterized argument. 3. Rerun only that argument to iterate quickly.
Filtering and grouping
- Use tag filters in navigator for focused development loops.
- Keep tag naming stable so teams can reuse filters and plans.
- Prefer tag-based include/exclude over fragile test-name patterns.
Suggested tag conventions
core- always-on fast checksintegration- external dependency coverageregression- bug-fix lock-in testsflaky- temporary quarantine while fixing
Test plans
- Configure include/exclude tags per target in test plans.
- Use "any tags" vs "all tags" intentionally when combining filters.
- Maintain separate plans for:
- fast core checks
- integration checks
- slower/optional scenarios
Example plan strategy
Coreplan: includecore, excludeintegration.Integrationplan: includeintegration, excludeflaky.ReleaseGateplan: includecoreandregression.
Report triage
- Review distribution insights for failure clustering by tags/bugs/destinations.
- Investigate grouped failures first (often indicates systemic regressions).
- Ensure disabled/known-issue reasons are visible and actionable in reports.
Triage sequence
1. Check if failures cluster by a shared tag. 2. Open one representative failure. 3. Confirm whether root cause is common (dependency/outage/config) or test-local. 4. Fix root cause, then remove temporary known-issue annotations.
Diagnostic quality
- Keep expectations expressive and narrow.
- Improve argument/type descriptions for faster root-cause identification.
- Ensure bug traits link to trackable issues.
Checklist
- Tag naming is consistent across suites.
- Test plans reflect team workflow (local dev, CI, release).
- Parameterized failures are rerun at argument-level before broad reruns.
Related skills
FAQ
When should I use #require instead of #expect?
Use #require when subsequent lines in the test depend on a prerequisite value; a failed #require stops the test immediately, preventing misleading downstream failures.
Should I annotate entire test suite types with @available for OS-gated tests?
No. Annotate individual test functions with @available for OS-gated behavior; never apply @available to suite types.
Can Swift Testing and XCTest coexist in the same test target?
Yes. The recommended migration is incremental: convert assertions first, then organize suites, then introduce parameterization and traits, keeping XCTest for UI automation and Objective-C code.
Is Swift Testing Expert safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.