Moving Beyond XCTest: A Complete Migration Guide to the Swift Testing Framework
For more than a decade, XCTest has been the default framework for testing Swift applications. But XCTest carries significant legacy Objective-C baggage: test classes had to inherit from XCTestCase, methods had to start with the word test, assertions were verbose (XCTAssertEqual), and parallel execution setup was complex.
In Swift 6, Apple introduced the new Swift Testing framework. Built from the ground up to utilize modern Swift features like macros, structured concurrency, and parameterization, Swift Testing makes unit tests cleaner, safer, and much faster.
In this guide, we will migrate a typical XCTest suite to the new Swift Testing APIs.
Comparing Syntax: XCTest vs. Swift Testing
Let’s look at the basic assertions side-by-side:
Legacy XCTest:
import XCTest
class CalculatorTests: XCTestCase {
func testAddition() {
let calc = Calculator()
XCTAssertEqual(calc.add(2, 3), 5, "Addition result should match")
}
}
Modern Swift Testing:
import Testing
struct CalculatorTests {
@Test func addition() {
let calc = Calculator()
#expect(calc.add(2, 3) == 5)
}
}
Notice the major improvements:
- No Class Inheritance: Test suites are standard Swift structs.
- Macros instead of prefix naming:
@Testmarks test functions, allowing you to name the method whatever you want. - Expressive Expectations: The
#expectmacro evaluates standard Swift boolean expressions and prints detailed diffs if they fail.
Parameterized Testing
One of Swift Testing’s best features is native support for parameterized tests. In XCTest, testing multiple inputs required manual loops or helper functions.
With Swift Testing, pass an array of inputs directly to the @Test macro:
struct AuthenticationTests {
@Test(arguments: ["admin", "user_12", "guest"])
func validateUsernameFormat(username: String) {
#expect(username.count >= 4)
}
}
Xcode runs each argument as a separate test run, showing you exactly which value failed.
Concurrency and Parallel Execution
Unlike XCTest which runs test suites sequentially on a single thread by default, Swift Testing executes tests in parallel using Swift Concurrency.
If your test suite relies on mutable global state that is not thread-safe, declare the suite with @Suite and configure it to run sequentially to prevent data races:
@Suite(.serialized)
struct DatabaseTests {
// Tests in this suite will run one after another
}
Migration Checklist
- Import
Testing: Replaceimport XCTestwithimport Testing. - Remove Class Wrappers: Change class declarations to structs or actors.
- Migrate Assertions: Convert
XCTAssertEqual,XCTAssertTrue, etc. to#expect(...). - Use
#require: For critical checks where a failure should stop the test execution immediately, use#require(...)instead of#expect. This throws an error and exits the function.