# Migrating from XCTest to Swift Testing > Convert XCTest unit tests to Swift Testing: @Test, #expect and #require, suites, parameterized tests, traits, async confirmations, and what stays in XCTest. - Canonical page: https://obelisk.club/blog/xctest-to-swift-testing-migration - Author: Peter Zhang - Published: 2026-11-01 - Tags: swift-testing, xctest, testing, xcode, swift, indie Swift Testing replaces `XCTestCase` classes with plain functions marked `@Test`, and the whole `XCTAssert` family with two macros: `#expect` (record a failure and keep going) and `#require` (throw and stop). Setup and teardown become `init()` and `deinit`. You can move one file at a time, because both frameworks can live in the same test target, even the same file. UI tests and performance tests stay in XCTest. To convert a file of XCTest code in your browser, try our [XCTest to Swift Testing converter](/tools/swift-testing-xctest). Swift Testing comes with Swift 6.0 and Xcode 16, and Apple's Xcode testing page says Xcode "continues to include XCTest to facilitate writing UI tests". ## Import and convert the class Import `Testing` instead of `XCTest`. If a file still holds tests from both frameworks, import both. Apple's migration article says a single source file can contain tests written with either. An `XCTestCase` subclass becomes a suite: remove the `XCTestCase` conformance. Apple recommends a struct or an actor over a class where you can, because the compiler can then check concurrency safety better. Marking the type with `@Suite` is optional; you add it to give the suite a display name or traits. A test is any function with `@Test`. It does not need a `test` prefix, and it can be `async`, `throws`, or isolated to a global actor. One difference to watch: XCTest runs synchronous test methods on the main actor, while Swift Testing runs test functions on an arbitrary task. If the test touches UI-bound code, add `@MainActor`. Each test method on a suite type runs on its own fresh instance of the type, so state never leaks from one test into the next. ## Before and after Here is an XCTest class with the usual setup, teardown, an equality check, a throwing call and an unwrap: ```swift import XCTest @testable import Cart final class CartXCTests: XCTestCase { var cart: Cart! override func setUp() { super.setUp() cart = Cart() } override func tearDown() { cart = nil super.tearDown() } func testAddIncreasesCount() { cart.add("tea") XCTAssertEqual(cart.items.count, 1) XCTAssertFalse(cart.items.isEmpty) } func testUnknownItemThrows() { XCTAssertThrowsError(try cart.price(of: "gold")) } func testLookup() throws { let price = try XCTUnwrap(cart.lookup("tea")) XCTAssertEqual(price, 3) } } ``` And the same tests in Swift Testing. A stored property with a value stands in for `setUp`, because every test gets a new instance: ```swift import Testing @testable import Cart @Suite("Cart") struct CartTests { let cart = Cart() @Test func addIncreasesCount() { cart.add("tea") #expect(cart.items.count == 1) #expect(!cart.items.isEmpty) } @Test func unknownItemThrows() { #expect(throws: CartError.self) { try cart.price(of: "gold") } } @Test func lookup() throws { let price = try #require(cart.lookup("tea")) #expect(price == 3) } } ``` ## Setup and teardown Apple's rule: implement `init()` and, if needed, `deinit` instead of `setUp()` and `tearDown()`. `init` may be `async` and `throws`. If you need teardown, declare the suite as a class or an actor, because only those can have a `deinit`. ```swift final class Scratch: @unchecked Sendable { static nonisolated(unsafe) var live = 0 init() { Scratch.live += 1 } deinit { Scratch.live -= 1 } } @Suite(.serialized) final class ScratchTests { let scratch = Scratch() deinit { /* clean up files or fixtures here */ } @Test func isLive() { #expect(Scratch.live >= 1) } } ``` ## Assertion mapping This is the table from Apple's migration article, trimmed to the rows you will meet most. | XCTest | Swift Testing | |---|---| | `XCTAssert(x)`, `XCTAssertTrue(x)` | `#expect(x)` | | `XCTAssertFalse(x)` | `#expect(!x)` | | `XCTAssertNil(x)` | `#expect(x == nil)` | | `XCTAssertNotNil(x)` | `#expect(x != nil)` | | `XCTAssertEqual(x, y)` | `#expect(x == y)` | | `XCTAssertNotEqual(x, y)` | `#expect(x != y)` | | `XCTAssertIdentical(x, y)` | `#expect(x === y)` | | `XCTAssertGreaterThan(x, y)` | `#expect(x > y)` | | `XCTAssertLessThan(x, y)` | `#expect(x < y)` | | `XCTAssertThrowsError(try f())` | `#expect(throws: (any Error).self) { try f() }` | | `XCTAssertNoThrow(try f())` | `#expect(throws: Never.self) { try f() }` | | `try XCTUnwrap(x)` | `try #require(x)` | | `XCTFail("...")` | `Issue.record("...")` | There is no direct counterpart of `XCTAssertEqual(_:_:accuracy:)`. Apple points to `isApproximatelyEqual()` from the swift-numerics package for comparing numbers within a tolerance. `#require` is also the replacement for `continueAfterFailure = false`: it throws a Swift error, and other tests keep running. ## Parameterized tests Where an XCTest loop hides which input failed, Swift Testing runs one test case per input and reports each one. Pass a collection to `arguments:`. Apple's documentation also shows enums that conform to `CaseIterable` (pass `allCases`, so a new case is tested without edits) and integer ranges. ```swift @Test("Known prices", arguments: [("tea", 3), ("cake", 5)]) func price(item: String, expected: Int) throws { #expect(try cart.price(of: item) == expected) } ``` By default, the cases of a parameterized test run in parallel with each other. ## Traits Traits are extra arguments to `@Test` or `@Suite`. A trait on a suite is inherited by its tests. ```swift import Foundation import Testing extension Tag { @Tag static var pricing: Self } @Suite(.tags(.pricing)) struct TraitTests { @Test(.disabled("Waiting for the new price list")) func futurePrices() { #expect(Bool(false)) } @Test(.bug("https://example.com/issues/42", "Price rounds wrong"), .timeLimit(.minutes(1))) func slowPath() { #expect(Cart().lookup("tea") == 3) } @Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_SLOW"] == nil)) func enabledByDefault() { #expect(true) } } ``` - `.disabled(_:)` skips a test with a comment, and `.enabled(if:)` runs it only when the condition holds. This replaces `XCTSkip`, `XCTSkipIf` and `XCTSkipUnless`. - `.bug(_:_:)` links a test to an issue tracker entry. - `.tags(_:)` takes `Tag` values you declare with the `@Tag` macro, to group and filter tests. - `.timeLimit(_:)` fails a test that runs too long. It is available from iOS 16, macOS 13 and watchOS 9. - `.serialized` makes the tests in a suite run one after another. Swift Testing runs tests in parallel by default, while XCTest runs the tests of a class one at a time, so add `.serialized` if your tests share global state. For an expected failure, `withKnownIssue { ... }` takes the place of `XCTExpectFailure`. Apple notes that the closure-less form of `XCTExpectFailure` has no direct equivalent; wrap the test body in `withKnownIssue` instead. ## Async callbacks with confirmation When you can, `await` the async function directly, or wrap a completion handler in a continuation. Apple's article recommends this over expectations. For events that arrive on their own, such as a handler that fires later, use `confirmation(_:expectedCount:)`, the counterpart of `XCTestExpectation`. ```swift @Test func changeHandlerFires() async { await confirmation("onChange called") { changed in cart.onChange = { changed() } cart.add("tea") } } @Test func changeHandlerFiresTwice() async { await confirmation(expectedCount: 2) { changed in cart.onChange = { changed() } cart.add("tea") cart.add("cake") } } ``` Two differences from expectations. A confirmation does not block or wait: the event has to happen before `confirmation` returns, or an issue is recorded. And the default is exactly one confirmation, like `XCTestExpectation`. For "at least" you can pass a range with a lower bound, for example `1...` or `0 ..< 100`; `expectedCount: 0` checks that something never happens. ## What stays in XCTest - **UI tests.** Apple's Xcode testing page says Xcode keeps XCTest for UI tests that control the app's interface. Apple's guide to adding tests also describes UI tests as `XCTestCase` subclasses. - **Performance tests.** Apple's performance-testing guide says they use the XCTest framework, with `measure` calls inside an `XCTestCase`. Swift Testing has no counterpart. - **Anything that needs XCTest-only APIs**, such as `XCTAssertEqual` with an accuracy argument. ## Run both side by side Apple's guide says Swift Testing and XCTest can build from the same test target and coexist inside one test bundle, so you do not need a new target. Convert files as time allows. Helpers can be shared. With interoperability, an `XCTAssert` call inside a helper that a Swift Testing test calls is reported as a failure, and a `#expect` inside a helper called from an XCTest method fails that method. The default mode depends on your toolchain: with a toolchain older than 6.4 it is `none`; with 6.4 or later it is `limited`, or `complete` when the package's `swift-tools-version` is 6.4 or later. Set `SWIFT_TESTING_XCTEST_INTEROP_MODE` to choose a mode yourself. The safest habit is to move shared helpers to `#expect` first, which Apple's article describes as a way to migrate your test infrastructure at your own pace: ```swift func assertUnique(_ elements: [Int]) { #expect(Set(elements).count == elements.count) } @Test func uniqueElements() { assertUnique([1, 2, 3]) } final class UniqueXC: XCTestCase { func testUnique() { assertUnique([1, 2, 3]) } func testMeasure() { measure { _ = (0..<1000).map { $0 * 2 } } } } ``` ## Sources All read on 1 November 2026. - [Migrating a test from XCTest](https://developer.apple.com/documentation/testing/migratingfromxctest), Swift Testing (mapping table, setup and teardown, interoperability, known issues) - [Testing](https://developer.apple.com/documentation/xcode/testing), Xcode - [Adding tests to your Xcode project](https://developer.apple.com/documentation/xcode/adding-tests-to-your-xcode-project), Xcode - [Writing and running performance tests](https://developer.apple.com/documentation/xcode/writing-and-running-performance-tests), Xcode - [Implementing parameterized tests](https://developer.apple.com/documentation/testing/parameterizedtesting) - [Organizing test functions with suite types](https://developer.apple.com/documentation/testing/organizingtests) - [Traits](https://developer.apple.com/documentation/testing/traits), including `timeLimit(_:)`, `disabled(_:sourceLocation:)`, `enabled(if:_:sourceLocation:)`, `bug(_:_:)` and `tags(_:)` - [Testing asynchronous code](https://developer.apple.com/documentation/testing/testing-asynchronous-code) *Apple's APIs and defaults change; check the pages above for current details.*