Swift Testing: The Complete Guide from Swift 6.0 to 6.2

Published on Β· Updated Β· 24 min

Wlad
Wlad
Founder & Swift Tech Lead

Swift Testing represents a revolution in the Apple testing ecosystem. Designed natively for Swift, this modern framework progressively replaces XCTest with an expressive syntax, powerful macros, and perfect integration with Swift Concurrency. This comprehensive guide covers everything you need to know, from fundamentals to advanced features in Swift 6.2.

Why Swift Testing?

After more than 10 years of XCTest, Apple has completely rethought the testing approach. Swift Testing is not an evolution of XCTest β€” it's a complete rewrite, designed for modern Swift.

The main motivations:

  • Declarative syntax with native Swift macros

  • First-class support for async/await and Swift Concurrency

  • Natively integrated parameterized tests

  • Flexible organization with tags and traits

  • Rich and contextual error messages

  • Open source and evolvable via Swift Evolution

The central philosophy: testing should not be a burden. Swift Testing makes writing tests as natural as writing production code.

Swift Testing vs XCTest

Before diving into details, here's a quick comparison of the two approaches:

AspectXCTestSwift Testing

Syntax

Classes, test* methods

Structs, @Test, @Suite

Assertions

XCTAssert* (30+ variants)

#expect(), #require()

Async

waitForExpectations

Native async/await

Parameterization

Manual (loops)

@Test(arguments:)

Organization

Class inheritance

Tags, traits, suites

Setup/Teardown

setUp(), tearDown()

init, deinit

Parallelization

Global configuration

Default, isolated

UI Tests

Yes

No (stays XCTest)

Performance Tests

Yes

No (stays XCTest)

Swift Testing and XCTest can coexist in the same project. UI and performance tests remain XCTest-exclusive.

Timeline and evolutions

Swift Testing has evolved rapidly since its introduction:

Swift 5.9 β€” Experimental introduction

First appearance of the framework in preview, allowing early adopters to test the approach.

Swift 6.0 β€” September 2024

Official release with Xcode 16. The framework becomes production-ready with:

  • @Test and @Suite macros

  • #expect() and #require() assertions

  • Parameterized tests

  • Basic traits (.enabled, .disabled, .bug, .timeLimit)

  • Tags for organization

Swift 6.1 β€” March 2025

Three major proposals integrated:

  • ST-0005: Ranged confirmations β€” Verify a callback is called N times

  • ST-0006: Return errors from expect throws β€” Retrieve the thrown error

  • ST-0007: Test Scoping Traits β€” Before/after code without global state

Swift 6.2 β€” September 2025

Significant evolutions with Xcode 26:

  • ST-0008: Exit Tests β€” Test crashes (precondition, fatalError)

  • ST-0009: Attachments β€” Attach files to test results

  • ST-0010: evaluate() on ConditionTrait

  • Raw identifier display names with backticks

Future roadmap

In active discussion:

  • ST-0011: Issue Handling Traits β€” Modify/filter issues before reporting

  • Test Issue Warnings β€” Issues that don't fail the suite

Installation and configuration

Package.swift

For a Swift Package, add the test dependency:

Xcode

In Xcode 16+, Swift Testing is automatically available. Simply create a file in your test target:

System requirements

To develop with Swift Testing:

  • macOS 14.5+ for Xcode 16

  • macOS 15+ for Xcode 26 (Swift 6.2)

  • The tested code can target older iOS/macOS versions

Coexistence with XCTest

You can have XCTest and Swift Testing files in the same target:

Both frameworks run together via swift test or the Xcode Test Navigator.

Fundamental syntax

@Test β€” Declaring a test

The @Test macro marks a function as a test:

Unlike XCTest, no need to prefix with test. The function name is free.

Display names

Add a readable name for reports:

Raw identifiers (Swift 6.2)

Swift 6.2 allows using backticks for expressive names:

The function name directly becomes the display name, without duplication.

@Suite β€” Organizing tests

Group related tests in a suite:

Suites can be nested:

Modern assertions

#expect() β€” Standard verification

#expect() is the main assertion. It accepts any boolean expression:

On failure, Swift Testing displays a rich contextual message showing actual values.

#require() β€” Mandatory preconditions

#require() stops the test immediately if the condition fails. Useful for preconditions:

#require() can also unwrap optionals:

Testing errors

To verify that a function throws an error:

Retrieving the error (Swift 6.1 β€” ST-0006)

Swift 6.1 allows retrieving the error for additional assertions:

XCTest β†’ Swift Testing mapping

XCTestSwift Testing

XCTAssertTrue(x)

#expect(x)

XCTAssertFalse(x)

#expect(!x)

XCTAssertEqual(a, b)

#expect(a == b)

XCTAssertNotEqual(a, b)

#expect(a != b)

XCTAssertNil(x)

#expect(x == nil)

XCTAssertNotNil(x)

#expect(x != nil)

XCTAssertGreaterThan(a, b)

#expect(a > b)

XCTAssertThrowsError(expr)

#expect(throws: Error.self) { expr }

XCTUnwrap(x)

try #require(x)

XCTFail("message")

Issue.record("message")

Asynchronous tests

Swift Testing natively supports async/await:

Complete API example

Timeouts

Use the .timeLimit trait to avoid blocking tests:

Parameterized tests

Parameterized tests run the same test with different inputs:

This test runs 5 times, once per argument.

Multiple arguments

Using CaseIterable

For enums, use allCases:

Parameter combinations

Test all combinations of two sets:

Zip for corresponding pairs

To test input/output pairs:

Traits β€” Test configuration

Traits modify the behavior of a test or suite.

.enabled(if:) and .disabled()

Conditionally enable or disable:

.bug() β€” Referencing issues

Document associated bugs:

.timeLimit() β€” Timeout

Set a time limit:

evaluate() for ConditionTrait (Swift 6.2)

Swift 6.2 allows evaluating conditions outside tests:

Tags β€” Flexible organization

Tags allow categorizing and filtering tests.

Creating tags

Define your tags in an extension:

Applying tags

Command line filtering

Tags view in Xcode

In the Test Navigator (⌘-6), click the tag icon to see your tests organized by tags rather than by file.

Advanced organization

Fixtures with init/deinit

Use struct initialization for setup:

Each test receives a fresh instance of the suite, thus a clean database.

Accessing the current test

For logging or debugging:

CustomTestStringConvertible

Customize type display in reports:

Confirmations and async expectations

confirmation() β€” Verifying callbacks

For callback-based APIs:

Ranged confirmations (Swift 6.1 β€” ST-0005)

Verify a callback is called a precise number of times:

withKnownIssue() β€” Known bugs

Mark a test as having a known bug without disabling it:

The test runs, the failure is recorded but doesn't fail the suite.

Test Scoping Traits (Swift 6.1 β€” ST-0007)

Test Scoping Traits allow running before/after code without mutable global state.

Defining a scope trait

Using the scope

Benefits:

  • No mutable global state

  • Compatible with parallelization

  • Reusable across suites

  • Uses Structured Concurrency

Exit Tests (Swift 6.2 β€” ST-0008)

Exit Tests allow verifying that code terminates the process (crash, precondition, fatalError).

Why it's revolutionary

Before Swift 6.2, testing a crash was nearly impossible. You had to create a separate CLI and use external tools. Now:

Practical example

How it works

The code in the block runs in a separate process. Swift Testing verifies the process exit code.

Attachments (Swift 6.2 β€” ST-0009)

Attachments allow attaching files to test results to facilitate diagnosis.

Attaching an image

Attaching JSON

Attaching logs

Attachments appear in Xcode and CI reports, greatly facilitating failure diagnosis.

Migration from XCTest

Progressive strategy

Don't migrate everything at once. Recommended approach:

  • New tests β†’ Swift Testing

  • Refactored tests β†’ Swift Testing

  • Stable tests β†’ Migrate progressively

  • UI/Performance tests β†’ Stay XCTest

Migration example

Before (XCTest):

After (Swift Testing):

What stays in XCTest

Some features remain XCTest-exclusive:

  • UI tests with XCUITest

  • Performance tests with measure {}

  • Certain CI-specific integrations

  • XCTActivity for log hierarchy

CI/CD integration

Xcode Cloud

Swift Testing works natively with Xcode Cloud. No special configuration required.

GitHub Actions

Test reports

Swift Testing supports multiple report formats:

Runtime Issue Detection (Xcode 26)

Xcode 26 introduces automatic detection of runtime issues in tests:

  • Thread Sanitizer violations

  • Memory leaks

  • Main thread violations

These issues appear as warnings or errors depending on your configuration.

Best practices

Naming conventions

Use descriptive English names:

One test = one logical assertion

Prefer multiple focused tests to one large test:

Test isolation

Each test must be independent:

Test performance

Parallelize as much as possible (it's the default). For tests that can't be parallelized:

Official resources

Apple documentation

WWDC Sessions

  • WWDC 2024 β€” Meet Swift Testing: Complete introduction

  • WWDC 2024 β€” Go further with Swift Testing: Advanced features

  • WWDC 2025 β€” What's new in Swift Testing: Exit tests, Attachments

GitHub

Swift Evolution Proposals

ProposalTitleVersion

ST-0005

Ranged confirmations

Swift 6.1

ST-0006

Return errors from expect throws

Swift 6.1

ST-0007

Test Scoping Traits

Swift 6.1

ST-0008

Exit tests

Swift 6.2

ST-0009

Attachments

Swift 6.2

ST-0010

evaluate() on ConditionTrait

Swift 6.2

ST-0011

Issue Handling Traits

In review

Community

Swift Testing represents the future of testing on Apple platforms. Its progressive adoption, coexistence with XCTest, and rapid evolutions make it an obvious choice for any new Swift project.