Moving Beyond XCTest: A Complete Migration Guide to the Swift Testing Framework

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: @Test marks test functions, allowing you to name the method whatever you want.
  • Expressive Expectations: The #expect macro 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

  1. Import Testing: Replace import XCTest with import Testing.
  2. Remove Class Wrappers: Change class declarations to structs or actors.
  3. Migrate Assertions: Convert XCTAssertEqual, XCTAssertTrue, etc. to #expect(...).
  4. 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.