Testing

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Swift has two testing frameworks in active use. Swift Testing (import Testing) is the modern, macro-based default for new packages and targets — see Macros for the @Test/#expect machinery underneath it. XCTest predates it, remains the framework for UI testing and much existing code, and is what swift test/Xcode ran exclusively before Swift Testing existed. Both run under the same swift test command and can coexist in one test target during a migration.

Swift Testing

import Testing
@testable import WeatherKit

@Test func parsesCelsius() {
    let reading = Reading(raw: "21.5C")
    #expect(reading.celsius == 21.5)
}

@Test func forecastRequiresACity() throws {
    let city = try #require(City(name: "Madrid"))   // unwraps, or fails and stops this test immediately
    #expect(city.name == "Madrid")
}

@Test marks an ordinary top-level or type-member function as a test case — no base class, no test-prefixed naming convention, and discovery is automatic. #expect(:) records a failure (with the failing expression’s sub-values captured automatically) but lets the test keep running; #require(:) does the same for an Optional or a Bool but throws on failure, stopping that test immediately — the right choice once a later line would crash or be meaningless without the value it unwraps.

@Suite and Parameterized Tests

@Suite("Reading parsing")
struct ReadingParsingTests {
    @Test("parses common temperature formats", arguments: [
        ("21.5C", 21.5), ("70F", 21.1), ("294K", 20.85)
    ])
    func parses(raw: String, expectedCelsius: Double) {
        let reading = Reading(raw: raw)
        #expect(abs(reading.celsius - expectedCelsius) < 0.1)
    }
}

@Suite groups related tests in a type (a struct, class or actor) and can nest suites inside suites, mirroring how the test report is organised. A parameterized test's arguments: runs the function once per element — each argument set is reported as its own test case, so one failing temperature format fails only that case rather than the whole function.

Traits

Trait Effect

.tags(.networking)

Attach a custom Tag for swift test --filter tag:networking-style selection.

.enabled(if: condition)

Skip the test at run time unless condition holds (e.g. a platform check).

.disabled("reason")

Always skip, with the reason shown in the test report.

.timeLimit(.seconds(5))

Fail the test if it runs longer than the limit.

.serialized

On a @Suite, force its tests to run one at a time instead of Swift Testing’s default parallel execution — for tests that share mutable state.

@Test(.tags(.networking), .timeLimit(.seconds(5)))
func downloadsForecast() async throws { ... }

@Suite(.serialized)
struct DatabaseTests { ... }        // these tests touch a shared file and must not interleave

confirmation, Exit Tests and Known Issues

@Test func downloadInvokesCallbackExactlyOnce() async {
    await confirmation(expectedCount: 1) { confirm in
        downloader.fetch { _ in confirm() }
    }
}

@Test func crashesOnCorruptHeader() async {
    await #expect(processExitsWith: .failure) {
        Reading(raw: "\u{0}\u{0}").parseOrFatalError()
    }
}

@Test func rateLimitingIsAKnownIssue() {
    withKnownIssue("rate limiter is disabled in CI, see TICKET-123") {
        #expect(rateLimiter.isEnabled)
    }
}

confirmation replaces XCTest’s XCTestExpectation for callback-based asynchronous code: the test fails if confirm() is not called the expected number of times before the closure returns. An exit test (#expect(processExitsWith:)) runs its body in a separate process and asserts on how that process terminates — the only way to test code that is expected to crash or call fatalError, without taking the whole test run down with it. withKnownIssue records a failure inside it as expected rather than a regression, so a genuinely broken but already-tracked behaviour doesn’t block the rest of the suite.

Attachments (Attachment) let a test save arbitrary data — a captured image, a log file, an intermediate JSON payload — alongside its result, viewable from Xcode’s or `swift test’s test report when diagnosing a failure after the fact.

Both swift test (which discovers and runs Swift Testing and XCTest tests in the same invocation) and Xcode’s Test navigator run Swift Testing tests identically; no separate command is needed.

XCTest

import XCTest
@testable import WeatherKit

final class ReadingTests: XCTestCase {
    var reading: Reading!

    override func setUp() {
        super.setUp()
        reading = Reading(raw: "21.5C")
    }

    override func tearDown() {
        reading = nil
        super.tearDown()
    }

    func testCelsiusIsParsed() {
        XCTAssertEqual(reading.celsius, 21.5, accuracy: 0.01)
    }

    func testDownloadCallsCompletionOnce() {
        let expectation = expectation(description: "download completes")
        downloader.fetch { _ in expectation.fulfill() }
        wait(for: [expectation], timeout: 5)
    }

    func testParsingPerformance() {
        measure {
            _ = Reading(raw: "21.5C")
        }
    }
}

XCTestCase is the base class every XCTest test type inherits; setUp/tearDown run around each test method (always call super first/last), and a test-prefixed method with no parameters is discovered automatically — the same shape as Objective-C’s XCTest, since it is the same framework. The XCTAssert… family (XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue, and so on) reports a failure at the exact call site with both the expected and actual values. XCTestExpectation (via expectation(description:) and wait(for:timeout:)) is XCTest’s answer to asynchronous callbacks — Swift Testing’s confirmation above is its direct successor. measure { …​ } runs its block repeatedly and reports a performance baseline, exactly as in Objective-C’s XCTest.

Apple’s official migration guide from XCTest to Swift Testing documents the mechanical translation for every construct above (XCTAssertEqual to #expect(==), XCTestExpectation to confirmation, and so on) — most existing XCTest suites can adopt Swift Testing incrementally, file by file, since both frameworks run side by side under swift test.

@testable import (see Attributes and Compiler Control) is what lets either framework’s test target reach a module’s internal declarations without those declarations being public — private/fileprivate symbols remain unreachable regardless.

XCTest also covers UI testing through XCUITest (XCUIApplication, element queries, tap()/typeText(_:)), which drives the app’s real UI in a separate process — this is Apple-platform-only (Xcode/iOS/macOS simulators and devices) and out of scope for this reference; see Apple’s XCTest — User Interface Tests documentation.

Running Tests

swift test                                        # every test target, both frameworks
swift test --filter ReadingParsingTests            # a suite/class by name
swift test --filter ReadingParsingTests/parses     # one test within it
swift test --filter tag:networking                 # by Swift Testing tag
swift test --parallel                              # explicit parallel execution across test targets
swift test --enable-code-coverage

In Xcode, ⌘U runs the whole plan and the diamond gutter icon next to a @Test/test… method runs that one test alone — both frameworks share the same Test navigator and report.

See Also

References

TSPL: no dedicated chapter — the testing libraries ship separately from the language; see Macros for the mechanism behind @Test, #expect and #require. Swift Testing documentation; XCTest reference; migrating from XCTest to Swift Testing.