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:
- We import
Testinginstead ofXCTest - The
@Testattribute replaces thetestprefix convention - 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,
withCheckedContinuationalso 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.
Start the conversation