Skip to main content

Swift Testing Reference

A complete API reference for Swift Testing — every macro, trait, and helper with its purpose, usage scenario, and a code example.

Declarations

Start here. Every test begins with a declaration — @Test marks the function, @Suite groups related tests together.

APIWhatWhen
@TestMarks a function as a testEvery test function you write
@Test("Name")Test with a custom display nameWhen the function name alone doesn’t capture the full intent
@SuiteGroups tests and applies shared traitsWhen you want to name a suite or attach traits to all tests inside
@Suite(.serialized)Suite whose tests run sequentiallyTests that share state and cannot safely run in parallel
// How
@Test func userCanLogin() async throws { }

@Test("User receives a welcome email after sign-up")
func userReceivesEmail() async throws { }

@Suite("Authentication")
struct AuthTests {
    @Test func validCredentials() { }
    @Test func wrongPassword() { }
}

@Suite(.serialized)
struct DatabaseMigrationTests {
    @Test func migrationV1() { }
    @Test func migrationV2() { }
}

Assertions

Once a test is declared, it needs to verify something. #expect is your default — it records failures without stopping the test so you can see all problems at once. Use #require only when a nil or thrown value would make every remaining assertion meaningless.

APIWhatWhen
#expect(expr)Non-fatal boolean assertionMost checks; test continues after failure so all issues are visible
#expect(throws: E.self) { }Asserts a specific error type is thrownTesting error paths where you care about the type
#expect(throws: errorValue) { }Asserts a specific error value is thrownTesting error paths where you care about the exact value (Equatable required)
#expect(throws: Never.self) { }Asserts no error is thrownConfirming a call that shouldn’t throw actually doesn’t
try #require(optional)Unwraps an optional or stops the test immediatelyWhen nil makes the rest of the test meaningless
try #require(throws: E.self) { }Requires an error and returns it for inspectionWhen you need to check the thrown error’s associated values
// How
#expect(result == 42)
#expect(user.isLoggedIn)
#expect(items.count > 0)

// Assert a specific error type
#expect(throws: AuthError.self) {
    try authService.login(password: "wrong")
}

// Assert a specific error value (AuthError must be Equatable)
#expect(throws: AuthError.invalidCredentials) {
    try authService.login(password: "wrong")
}

// Assert no error is thrown
#expect(throws: Never.self) {
    try validOperation()
}

// Unwrap optional or stop test
let user = try #require(fetchedUser)
#expect(user.name == "Ada")

// Require an error and inspect it
let error = try #require(throws: AuthError.self) {
    try authService.login(password: "wrong")
}
#expect(error == .invalidCredentials)

Traits

With declarations and assertions in place, use traits to control when and how a test runs — without cluttering the test body with conditional logic.

APIWhatWhen
.disabled("reason")Skips a test with a recorded reasonTest is blocked on an external dependency or tracked bug
.enabled(if: condition)Runs only when a runtime condition is trueTests that require a specific environment variable or capability
.timeLimit(.minutes(1))Fails the test if it exceeds a durationAsync tests that could hang and block CI
.tags(.myTag)Attaches a label for filteringOrganising tests by category, speed tier, or CI lane
// How

// Skip with a reason
@Test(.disabled("Waiting on API contract — issue #55"))
func paymentWebhookHandling() { }

// Conditional on environment
@Test(.enabled(if: ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] == "1"))
func stripeWebhookReachable() async throws { }

// Fail if too slow
@Test(.timeLimit(.seconds(30)))
func largeFileUpload() async throws { }

// Tag for CI filtering
extension Tag {
    @Tag static var unit: Self
    @Tag static var integration: Self
    @Tag static var slow: Self
}

@Test(.tags(.unit, .auth))
func validCredentialsLogIn() { }

@Suite(.tags(.integration))
struct NetworkTests { }

Parameterised Tests

If the same assertion needs to hold for multiple inputs, don’t copy-paste the test — parameterise it. Each argument set becomes its own independent test case in the results.

APIWhatWhen
@Test(arguments: [...])One test case per element in a collectionValidating a fixed set of inputs against the same assertion
@Test(arguments: zip(...))One test case per paired input/expected valueMapping known inputs to known outputs — keep expected values as literals
@Test(arguments: col1, col2)One test case per combination of two collectionsTesting every method × input pair (Cartesian product — grows fast)
// How

// Single collection
@Test("Valid currency codes are accepted", arguments: ["USD", "EUR", "GBP", "JPY"])
func validatesCurrencyCode(code: String) {
    #expect(CurrencyValidator.isValid(code))
}

// Paired values — expected is a hardcoded literal, never computed
@Test(
    "Each error code maps to the correct message",
    arguments: zip(
        [400,           401,            403,         404],
        ["Bad Request", "Unauthorised", "Forbidden", "Not Found"]
    )
)
func errorCodeMapsToMessage(code: Int, message: String) {
    #expect(HTTPError(code: code).localizedDescription == message)
}

// Cartesian product
@Test(
    "All payment methods work with all currencies",
    arguments: [PaymentMethod.card, .applePay, .payPal],
             ["USD", "EUR", "GBP"]
)
func paymentMethodWithCurrency(method: PaymentMethod, currency: String) async throws {
    let result = try await checkout.process(method: method, currency: currency)
    #expect(result.isSuccess)
}

Async and Events

Synchronous assertions are straightforward, but testing async callbacks and publishers requires a different tool. confirmation lets you assert that a closure fires a specific number of times, with Swift’s concurrency keeping everything structured.

APIWhatWhen
try await confirmation { }Asserts an async callback fires exactly onceTesting delegates, closures, or publishers that should emit one event
try await confirmation(expectedCount: N) { }Asserts an async callback fires exactly N timesTesting repeated emissions like progress updates
// How

// Assert a callback fires once
@Test
func loginPublishesSuccessEvent() async throws {
    try await confirmation { confirmed in
        authService.onLoginSuccess = { confirmed() }
        try await authService.login(email: "ada@example.com", password: "correct")
    }
}

// Assert a callback fires exactly N times
@Test
func uploadProgressFiresThreeTimes() async throws {
    try await confirmation(expectedCount: 3) { confirmed in
        uploader.onProgress = { _ in confirmed() }
        try await uploader.upload(file: largeFile)
    }
}

Known Issues

Not every failure is worth fixing right now. withKnownIssue lets you document a known bug inline — the test still runs, the failure is recorded, but the suite stays green until the bug is actually fixed.

APIWhatWhen
withKnownIssue("msg") { }Expects a failure without failing the test run; warns on unexpected successTracking a known bug that is out of scope to fix now
withKnownIssue(isIntermittent: true) { }Suppresses a failure that only occurs sometimesAcknowledging a flaky test while keeping it in the suite
// How

// Document a known bug
@Test
func parserHandlesEmojiInUsernames() {
    withKnownIssue("Parser crashes on emoji — issue #482") {
        let result = parser.parse("user🚀name")
        #expect(result != nil)
    }
}

// Acknowledge intermittent flakiness
withKnownIssue(isIntermittent: true) {
    #expect(flakeyService.ping())
}

Custom Traits

When you find yourself writing the same setup and teardown logic across many tests — network stubbing, database seeding, feature flag overrides — extract it into a custom trait. A custom trait is reusable, composable, and attaches to any @Test or @Suite as a single annotation.

APIWhatWhen
TestTrait + TestScopingA reusable setup/teardown wrapper attachable to any @Test or @SuiteCross-cutting concerns like network stubbing, DB seeding, or feature flag overrides
// How

struct NetworkStubTrait: TestTrait, TestScoping {
    func provideScope(
        for test: Test,
        testCase: Test.Case?,
        performing function: @Sendable () async throws -> Void
    ) async throws {
        NetworkStubber.activate()
        defer { NetworkStubber.deactivate() }
        try await function()
    }
}

extension Trait where Self == NetworkStubTrait {
    static var stubbedNetwork: Self { NetworkStubTrait() }
}

// Usage
@Test(.stubbedNetwork)
func fetchesUserFromStubbedAPI() async throws {
    NetworkStubber.stub(url: "/users/me", response: User.fixture)
    let user = try await api.currentUser()
    #expect(user.name == "Ada")
}