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
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.
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.
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
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.
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
Swift Testing clearly visualizes the possible states of test results, helping you understand outcomes at a glance.
Possible States of Test Results
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
Swift Testing supports parallel execution by default, even on physical devices!
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
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
Reference
StackOverflow: Is Swift Testing available on iOS 15 / Xcode 15?
Building Blocks of Swift Testing
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
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
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
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.
struct CommentTraitExamples {
@Test func loginTest() {
#expect(true)
}
}
Test Names
Test Navigator:
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
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 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.
.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
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
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.
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
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.
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
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.
@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.
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
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 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
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.
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.