How to use Swift Testing

Issue #1007

How to Use Swift Testing

Testing in Swift has always required inheriting from XCTestCase and prefixing every method with test. Sound familiar? With Swift 6, Apple introduced Swift Testing — a framework that feels native to modern Swift and eliminates much of the ceremony we’ve grown accustomed to.

In this article, we’ll explore the key patterns that make Swift Testing more expressive and how to migrate your existing tests.

The Shift from Classes to Attributes

Let’s start with the most fundamental change. Instead of class inheritance and naming conventions, Swift Testing uses attributes to mark test methods:

import Testing

struct UserTests {
    @Test func createsUserWithValidEmail() {
        let user = User(email: "test@example.com")
        #expect(user.isValid)
    }
}

That does a few important things:

  1. We import Testing instead of XCTest
  2. The @Test attribute replaces the test prefix convention
  3. Our container is a struct, giving us automatic isolation between tests

The good news, though, is that you can use either structs or classes. Structs are recommended for most cases because each test gets a fresh instance. Classes work better when you need reference semantics or shared resource management across tests.

A Better Assertion System

One mistake that’s sometimes made with XCTest is using the wrong assertion function — XCTAssertTrue when you meant XCTAssertFalse, or forgetting which parameter comes first. Swift Testing simplifies this with two powerful macros.

#expect handles standard assertions with natural Swift syntax:

@Test func validatesPricing() {
    let cart = Cart(items: [item1, item2])

    #expect(cart.total == 29.99)
    #expect(cart.items.count > 0)
    #expect(cart.discount != nil)
}

#require combines assertion with optional unwrapping, stopping the test immediately if the value is nil:

@Test func parsesConfiguration() throws {
    let config = try #require(Config.load(from: "settings.json"))

    #expect(config.apiEndpoint.contains("https"))
}

For cases where you need to explicitly record a failure within conditional logic, use Issue.record():

@Test func handlesEdgeCases() {
    switch result {
    case .success(let value):
        #expect(value > 0)
    case .failure:
        Issue.record("Expected success but got failure")
    }
}

Testing Async Code

Swift Testing embraces modern concurrency patterns. For testing async functions, you can use async/await directly:

@Test func fetchesUserProfile() async throws {
    let profile = try await api.fetchProfile(id: "123")
    #expect(profile.name == "Alice")
}

For callback-based APIs, the framework provides a Confirmation API:

@Test func notifiesOnCompletion() async {
    await confirmation { completed in
        downloader.onComplete = { completed() }
        downloader.start()
    }
}

Important: Confirmation closures require at least one await statement. For single-callback scenarios, withCheckedContinuation also works well.

Organizing Tests at Scale

As your test suite grows, Swift Testing offers several organization tools.

Suites group related tests logically:

@Suite("Authentication Tests")
struct AuthTests {
    @Test func validatesCredentials() { ... }
    @Test func handlesExpiredTokens() { ... }
}

Parameterized testing lets you run the same test with different inputs:

@Test(arguments: ["user@email.com", "admin@site.org", "test@domain.net"])
func acceptsValidEmails(_ email: String) {
    #expect(Email(email).isValid)
}

Tags enable selective test execution by category:

@Test(.tags(.critical))
func processesPayment() { ... }

@Test(.tags(.integration))
func syncsWithServer() { ... }

You can disable tests conditionally using .disabled() or .enabled(if:):

@Test(.disabled("Waiting for API v2"))
func usesNewEndpoint() { ... }

@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil))
func runsOnlyInCI() { ... }

Bringing It Together

Swift Testing represents a significant step forward for testing in Swift. The attribute-based syntax feels more natural, the assertion macros provide better error messages, and the async support aligns with modern Swift patterns.

In your own projects, you can migrate incrementally — Swift Testing and XCTest coexist peacefully in the same target. Start with new tests, then gradually convert existing ones as you touch that code.

The framework continues to evolve, and these patterns will serve as a foundation as Apple expands its capabilities in future releases.

Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation