Home Articles

Swift Testing

Introduction

Swift Testing has been officially integrated into Xcode 16

Apple released Swift Testing in WWDC 24

You can watch the official sessions here for deeper insights:

Swift Testing - A Modern Testing Framework

Swift Testing - A Modern Testing Framework

Importance of Testing

Writing tests is crucial for ensuring

  • App Quality

  • Reliability

Automated testing has been proven to:

  • Achieve consistent quality

  • Maintain Software Quality over time.

It is also essential for implementing effective CI/CD pipelines and enabling automated deployments.

XCTest(Traditional)

  • XCTest is a widely used and mature testing framework.

  • Supports writing unit tests, integration tests, UI tests, and performance tests.

Swift Testing (New, Swift 6+)

  • Swift Testing is a new, modern testing framework built for Swift 6+.

  • Leverages the power of Swift language features to write tests in a more expressive, Swift-native way.

Challenges in Testing

Some of the common testing challenges include:

  • Readability: Test cases can become hard to understand.

  • Code Coverage: Ensuring sufficient coverage is often difficult.

  • Organization: Tests can become disorganized as projects scale.

  • Fragility: Tests might fail unpredictably due to external dependencies or unstable conditions.

Qualities Desired in Test code

Good test code should be:

  • Structured: Easy to follow and maintain

  • Organized: Logically grouped and well-named

  • Self-Documenting: Clear enough to understand without external documentation

Goal of Unit Testing

The ultimate goal of unit testing is to:

  • Enable sustainable growth of your software project.

  • Make it easy to start a project from scratch — but more importantly, help you maintain and scale it over time.

About Swift Testing

Swift Testing is Apple’s latest framework for writing tests in Swift.

Compared to the traditional XCTest framework, it offers:

  • A modern, expressive, and Swifty approach to writing tests

  • An independent framework, no longer tightly coupled with Objective-C legacy

  • Enhanced code quality and developer experience

Interpret test results

Swift Testing clearly visualizes the possible states of test results, helping you understand outcomes at a glance.

Possible States of Test Results

Possible States of Test Results

High Level Comparison

XCTest

  • Available on: Apple Platforms (iOS, macOS, tvOS, watchOS)

  • Supports: Unit tests, performance tests and UI Tests

  • Based On: Objective-C

Swift Testing

  • Available on: Platforms that support Swift 6.0, Linux, Windows (Open Source)

  • Supports: Unit and Integration Tests

  • Based On: Swift-powered, with modern concurrency & Macros

Parallel testing on physical devices

Swift Testing supports parallel execution by default, even on physical devices!

Parallel Testing

Parallel Testing

If you’re building a multi-platform app, you can run your entire test suite across all target devices — iPhone, Apple Watch, and even visionOS — in parallel, at the same time.

Apple Platform Devices

Apple Platform Devices


Deployment Target vs. Environment Requirements

Swift Testing does require specific tooling and OS versions for the development environment where the tests are run:

  • Swift 6+

  • Xcode 16+

  • macOS 14.5 or later

So, while your application can still support older OS versions (like iOS 15), you need to run tests on a modern development environment.

Environment Requirements

Environment Requirements

Reference

StackOverflow: Is Swift Testing available on iOS 15 / Xcode 15?

Building Blocks of Swift Testing

Building Blocks of Swift Testing

Building Blocks of Swift Testing

@Test

How to create a simple Test

With Swift’s new testing capabilities, you can now write clean, expressive tests without the boilerplate you’re probably used to.


Just Import and Write

To get started, simply import the testing framework in your test target. No extra configurations or base classes needed. Then, define a test function and annotate it with the @Test macro. That’s it — you’re done!

import Testing
@Test
func testAddition() {
    let result = sum(2, 2)
    #expect(result == 4)
}

Simple Test

There’s no need to create a class or inherit from XCTestCase. Once you’ve added the function with the @Test macro, Xcode will automatically recognize it in the Test Navigator, just like you’re used to with XCTest.

Test Navigator

Test Navigator

What is @Test

The @Test attribute is the foundational building block of Swift Testing.

  • Defines ordinary Swift functions as test cases

  • Can be written as global functions, instance methods, or static methods

  • Supports async, throws, and actor annotations

  • Import test targets using the @testable attribute

💡

Tip: It is recommended to use Structures or Actors for better isolation and value semantics.

Here’s the macro definition for those curious:

@attached(peer)
macro Test(
_ displayName: String? = nil,
_ traits: any TestTrait...
)

attached Macro - @Test

Assertions with #expect

Instead of using multiple assertion methods like XCTAssertEqual, XCTAssertNil, or XCTAssertTrue, Swift’s testing framework provides a unified macro: #expect.

This macro:

  • Accepts any Boolean condition.

  • Replaces more than 40 different XCTest assertions with one powerful and flexible tool.

  • Allows optional custom failure messages, making debugging easier:

Macro definition for reference:

@freestanding(expression)
public macro expect(
_ condition: Bool,
_ comment: @autoclosure () -> Testing.Comment? = nil,
sourceLocation: Testing.SourceLocation = #_sourceLocation
)

freestanding macro - #expect


Grouping Tests Together

You can define multiple test functions within the same file, like so:

struct ArithmeticOperationsTests {
    @Test
    func testAddition() {
        let result = sum(2, 2)
        #expect(result == 4)
    }
    @Test
    func testSubtraction() {}
}

Grouping Tests

Advantage: In case of failure, you get a detailed breakdown of the expression and its values.

Organizing Your Tests with Structs (or Even Actors!)

Swift’s modern testing framework not only simplifies how we write tests, but also how we organize them. No more subclassing XCTestCase, and no more boilerplate. You can now group related tests into any Swift type — even structs or actors.

struct ArithmeticOperationsTests {
    @Test
    func testAddition() {
        let result = sum(2, 2)
        #expect(result == 4)
    }
    @Test
    func testSubtraction() {}
}

Organizing Tests

No inheritance. No XCTestCase. These can be structs, classes, or even actors.

Xcode will recognize this structure and show it grouped under the test name (ArithmeticOperationsTests) in the Test Navigator:

Test Navigator

Test Navigator

📦 Can I Write Tests in My App Target?

You might ask:

“Hey, can I just write tests inside my app bundle?”

Nope.❌

Swift’s testing framework must be used in test targets only. Why?

  • Test code won’t be stripped in release builds.

  • It could bloat your binary or expose test logic in production.

👉 Always keep tests in a separate test target.

Customise Test names

struct CommentTraitExamples {
    @Test func loginTest() {
        #expect(true)
    }
}

Test Names

Test Navigator:

Test Navigator for simple struct

Test Navigator for simple struct

Naming is one of the hardest things in software engineering. Swift Testing gives you a nice way to make your test names more expressive using the display name parameter in the @Test macro.

You can customize test names by passing a string literal:

struct CommentTraitExamples {
    @Test("Verifies that login flow works correctly")
    func loginTest() {
        #expect(true)
    }
}

Customized Test names

This shows up in the Test Navigator like this:

Customized Test name example

Customized Test name example

#expect vs #require macro

#require

let user = try #require(repository.getUser(id: 1))

Require Macro Example

#require is used when your test must not proceed if a condition isn’t met. It’s Swift’s answer to XCTUnwrap from XCTest, but more expressive and better integrated into the new macro-based syntax.

The #require macro is designed to:

  • Immediately stop test execution when a condition fails.

  • Automatically unwrap optionals or throw a failure if the value is nil.

  • Provide more precise and focused test outcomes.


Declaration:

@freestanding(expression)
public macro require(
  _ condition: Bool,
  _ comment: @autoclosure () -> Testing.Comment? = nil,
  sourceLocation: Testing.SourceLocation = #_sourceLocation
)

Require Macro Documentation

@Test func checkEmailValidation() throws {
    // First we require that the user exists
    let user = try #require(repository.getUser(id: 1))
    
    // Then we require that validating the email doesn't throw
    let email = try #require(try user.validateEmail())
    
    // If we get here, we know email validation succeeded
    #expect(email == "test@example.com")
}

#require vs #expect Macros

Why Use #require

  • It simplifies optional unwrapping and error handling.

  • It prevents misleading test results caused by subsequent code running after a critical failure.

  • It keeps your test logic clean and expressive.

Advantage: If any #require fails, the test stops immediately, providing better error isolation.

#expect

Behavior: Logs failure, continues execution

When to Use: Multiple checks in one test

#require

Behavior: Stops test if condition fails

When to Use: Critical preconditions or unwraps

Traits

Traits are a way to customize the behavior of individual test cases and adds descriptive information to tests

You can use traits to:

  • Disable a test temporarily

  • Skip tests under specific conditions

  • Group or tag tests

  • Control test execution behavior (e.g., timeouts, retries in future updates)

They make your tests more expressive, flexible, and self-documenting.

Conditional Trait

.disabled

This helps you skip a test while keeping it in your codebase.

@Test(.disabled("This feature is under development"))
func featureUnderDevelopment() {
    // This code will never run - the test will be skipped
    #expect(false) // Would fail if it ran, but it won't
}

.disabled Trait example

From Apple Documentation

static func disabled(
    if condition: @autoclosure @escaping () throws -> Bool,
    _ comment: Comment? = nil,
    sourceLocation: SourceLocation = #_sourceLocation
) -> Self

.disabled Trait Apple Documentation

Here’s how it looks in practice:

.disabled Trait - Test Navigator

.disabled Trait - Test Navigator

  • featureUnderDevelopment() is greyed out and skipped

  • Other tests like cameraPermissionTest() and paymentProcessingTest() are still active

.enabled

Sometimes you only want a test to run under specific conditions—like on a real device. That’s where the .enabled trait comes in handy.

// Test that only runs on real devices
@Test(.enabled(if: isPhysicalDevice(), "Camera access only available on physical devices"))
func cameraPermissionTest() {
    // This test will only run on physical devices, not in    simulators
    // Test camera permission logic here
    #expect(true)
}

.enabled Trait Example

With .enabled, the test is skipped unless the condition is met. Perfect for tests that rely on hardware features like camera, biometrics, or sensors.

From Apple Documentation

static func enabled(
    if condition: @autoclosure @escaping () throws -> Bool,
    _ comment: Comment? = nil,
    sourceLocation: SourceLocation = #_sourceLocation
) -> Self

.enabled Trait

TimeLimitTrait

In traditional XCTest, managing timeouts requires expectations and manual waits:

class LocalProcessingTimeoutXCTest: XCTestCase {
    func testDataProcessingTimeout() {
        let expectation = XCTestExpectation(
		description: "Data processing completed”
	)
        let processor = DataProcessor()
        Task {
            do {
                let result = try await processor.processDataset()
                XCTAssertTrue(result.count > 0)
                expectation.fulfill()
            } catch {
                XCTFail("Processing failed")
            }
        }
        wait(for: [expectation], timeout: 0.1)
    }
}

XCTest - TimeLimit

In Swift Testing Framework,

struct LocalProcessingSwiftTesting {
    @Test(.timeLimit(.minutes(1)))
    func testDataProcessing() async throws {
        let processor = DataProcessor()
        let result = try await processor.processDataset()
        #expect(result.count > 0, "Should find some prime numbers")
    }
}

Swift Testing - .timeLimit Trait

💡

Note: timeLimit only accepts durations of 1 minute or more—no sub-minute precision like 500ms.

This is intentional to keep the framework lightweight and avoid unnecessary complexity.

Tag.List

A type representing one or more tags applied to a test.

Swift Testing introduces a powerful way to categorize and filter tests using the @Tag attribute. Tags allow you to group tests based on type, feature, or behavior—for better readability and easier execution.

Tag Extension

Tag Extension

These are type-safe, compiler-checked, and easy to manage.

Tagging Individual Tests

Once defined, you can apply tags using the .tags trait:

struct TaggingIndividualTests {
    
    @Test(.tags(.ui))
    func testUIComponents() { … }
    
    
    @Test(.tags(.network, .performance))
    func testNetworkPerformance() { … }
    
    
    @Test(.tags(.flaky), .timeLimit(.minutes(1)))
    func testFlakyWithTimeLimit() { … }
    
}

tags Trait Example

  • Multiple tags can be applied to a single test

  • You can combine tags with other traits like .timeLimit

  • Tags help isolate and run targeted groups of tests quickly

Why Use Tags?

Tags give you the flexibility to:

  • Group regression or flaky tests

  • Isolate module-specific tests like auth, payment, networking

  • Create fast-feedback suites by tagging critical paths

  • Improve test navigation in large projects

And the best part? Once you’ve added tags, they appear in Xcode’s Test Navigator, letting you run them independently.

Bug

A type representing a bug report tracked by a test.

One of the standout features in the new Swift Testing framework is the ability to link tests to bug reports—something that was missing in traditional XCTest.

// Real bug that cost us $50k in lost revenue last quarter when customers
// with apostrophes in their names couldn't complete transactions
@Test(.bug("https://github.com/myorg/myrepo/issues/789"))
func testNameWithSpecialCharacters() {
    let processor = PaymentProcessor()
    let payment = Payment(customerName: "M'Kathir", amount: 99.99)
    let result = processor.processPayment(payment)
    #expect(result.succeeded)
}

Bug Example

With just .bug("url"), you can associate your test with a real issue.

You can link to GitHub, JIRA, or any issue tracker.

Syntax

static func bug(
    _ url: String,
    _ title: Comment? = nil
) -> Bug

Bug - Apple Documentation

Why Is This Useful?

  • Traceability - Directly connect bugs to tests for future reference.

  • Documentation - Capture business-impacting issues right in the test itself.

  • Release Notes - Make automated changelogs more powerful by referencing linked bugs.

  • Team Awareness - New team members or reviewers immediately understand why a test exists.

Custom Traits

@Test(.offline)
func offlineCachingTest() async throws {
    // No need to manually set up offline mode - the trait handles it!
    let apiClient = NetworkClient.shared
    let result = await apiClient.fetchData()
    #expect(result.source == .cache)
}

Custom Trait

In this example:

I created a custom .offline trait that stubs network status as offline, so the entire test runs in that condition — no manual setup required!

Curious how to make your own traits?

Yes, it’s possible! You’re not limited to built-in ones like .tags or .timeLimit.

Real-World Use Cases for Custom Traits

You can go beyond just offline/online stubbing — here are common categories for creating custom traits:

  • Device-Specific Tests

  • Environment-Specific Tests

  • Authentication-Based Tests

  • Database Setup & Cleanup

  • Resource Management

SPONSOR

iOS Conference AI Image

The Unique iOS Swift Conference in the UK

SwiftLeeds is a premier iOS conference taking place on October 7-8 this year. If you’re looking to stay updated on the latest trends in iOS development and connect with like-minded professionals, this event is a must-attend! Don’t miss out—book your tickets now!

Get your tickets!

Suites

Suites group related tests together, improving organization and clarity.

  • @Test functions and nested @Suites are automatically considered part of the suite.

  • Suites can have stored properties and lifecycle methods (init / deinit) for setup/teardown logic.

  • Each test method gets its own instance, isolating state safely.

💡

Embrace value semantics: Use structs to keep state management clean and isolated.

@Suite("User Authentication Tests")
struct AuthenticationTests {
    @Test func userCanSignIn() {
        let isSignedIn = true
        #expect(isSignedIn)
    }

    @Test func userCanSignOut() {
        let isSignedOut = true
        #expect(isSignedOut)
    }
}

Suite Example

Final Notes

Swift Testing modernizes your testing workflow:

  • Cleaner syntax

  • Better concurrency support

  • Parallel testing by default

  • Improved error messages

As Swift continues to evolve, adopting Swift Testing will future-proof your testing strategy.

Summary

If you’re short on time, here’s the quick takeaway:

  • Swift Testing is a modern, Swift-native framework for writing tests introduced in Xcode 16.

  • Replaces parts of XCTest with cleaner syntax, better concurrency, and default parallel execution.

  • @Test for defining test cases

  • #expect and #require macros for readable assertions

  • Traits to customize test behavior

  • Suites for grouping related tests

  • Swift Testing supports running tests in parallel across devices, including visionOS, iOS, and Apple Watch.

  • Designed for maintainable, scalable, and self-documenting test codebases.

  • Recommended for any Swift project moving forward!

Note

Please find the GitHub Repo

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.