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 |
|---|---|
|
Attach a custom |
|
Skip the test at run time unless |
|
Always skip, with the reason shown in the test report. |
|
Fail the test if it runs longer than the limit. |
|
On a |
@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
-
Macros — how
@Test/#expect/#requireexpand, and testing a macro’s own expansion. -
Swift Package Manager —
.testTargetandswift testas a package command. -
Attributes and Compiler Control —
@testable importin full. -
Async/Await and Tasks —
async/awaitinside a@Testfunction.