Skip to content

← All tools

Free reference for Apple developers

Swift Testing reference and XCTest converter

Swift Testing needs Swift 6.0 and Xcode 16 or later. Following Apple's migration guide: XCTAssertEqual(a, b) becomes #expect(a == b), XCTAssertNil(x) becomes #expect(x == nil), try XCTUnwrap(x) becomes try #require(x), XCTFail("…") becomes Issue.record("…"), setUp becomes init, tearDown becomes deinit, expectations become confirmation, and XCTSkipIf becomes the .disabled(if:) trait. UI tests and performance tests stay in XCTest. Paste a test class below to convert it; the lines it cannot convert are marked and explained. Under it: Apple's full mapping table and a reference for every Swift Testing feature.

Convert an XCTest class

Paste a Swift file with one or more XCTestCase classes. It runs in this page; nothing is uploaded.

Swift Testing version

Paste an XCTestCase class above to see the Swift Testing version.

XCTest to Swift Testing, side by side

From Apple's "Migrating a test from XCTest". The first rows are Apple's assertion table as written. Apple's page

XCTestSwift TestingNote
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)
XCTAssertNotIdentical(x, y)#expect(x !== y)
XCTAssertGreaterThan(x, y)#expect(x > y)
XCTAssertGreaterThanOrEqual(x, y)#expect(x >= y)
XCTAssertLessThanOrEqual(x, y)#expect(x <= y)
XCTAssertLessThan(x, y)#expect(x < y)
XCTAssertThrowsError(try f())#expect(throws: (any Error).self) { try f() }
XCTAssertThrowsError(try f()) { error in … }let error = #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("…")
XCTAssertEqual(x, y, accuracy: e)—No equivalent. Apple points to isApproximatelyEqual() in swift-numerics.
import XCTestimport TestingA file can import both while you move tests over.
class FoodTruckTests: XCTestCasestruct FoodTruckTestsRemove the XCTestCase conformance. Apple recommends a struct or actor so the compiler can check concurrency safety.
func testEngineWorks()@Test func engineWorks()No test prefix needed. XCTest runs synchronous tests on the main actor; Swift Testing does not, so add @MainActor where needed.
override func setUp() async throwsinit() async throws
override func tearDown()deinitNeeds a class or actor (final class FoodTruckTests).
continueAfterFailure = falsetry #require(…)
try XCTSkipIf(x) / try XCTSkipUnless(x)@Test(.disabled(if: x)) / @Test(.enabled(if: x))
throw XCTSkip("…")try Test.cancel("…")Swift 6.3, Xcode 26.4 or later.
XCTExpectFailure("…") { … }withKnownIssue("…") { … }
XCTExpectFailure("…", options: .nonStrict()) { … }withKnownIssue("…", isIntermittent: true) { … }
options.isEnabled / options.issueMatcherwithKnownIssue { … } when: { … } matching: { issue in … }
XCTExpectFailure(_:options:) (no closure)—No direct equivalent. Wrap the whole test body in withKnownIssue.
expectation(description:) / fulfill() / fulfillment(of:)await confirmation("…") { done in … done() }Confirms the event happened while the closure ran. A range count needs Swift 6.1.
expectedFulfillmentCount = 10confirmation(expectedCount: 10)
assertForOverFulfill = falseconfirmation(expectedCount: 10...)
(tests in a class run one after another)@Suite(.serialized)Swift Testing runs a suite's tests in parallel by default.
XCTAttachment + add(_:)Attachment.record(…)
XCUIApplication (UI tests)—Stays XCTest. Apple: Xcode keeps XCTest for UI tests. Apple's page
measure { … } (performance tests)—Stays XCTest. Apple's performance tests use measure(_:) in XCTest. Apple's page

Swift Testing reference

What each piece does, with Apple's example and the first Swift and Xcode version that has it, as Apple's page for that symbol states.

@Test

Marks a function as a test. It can be a global function or a method; the name does not need a test prefix. Pass a display name as the first argument.

@Test func engineWorks() {
  ...
}

@Test("Food truck exists") func foodTruckExists() { ... }

Swift 6.0, Xcode 16.0 Apple's page

@Suite

Groups tests in a type. Any type with @Test methods is a suite; @Suite lets you add a display name or traits.

@Suite struct FoodTruckTests {
  @Test func engineWorks() { ... }
}

Swift 6.0, Xcode 16.0 Apple's page

#expect

Checks a condition and records a failure with the values it saw. The test keeps running.

#expect(calculator.total(of: [3, 3]) == 7)
// Expectation failed: calculator.total(of: [3, 3]) == 7

Swift 6.0, Xcode 16.0 Apple's page

#require

Like #expect, but throws and stops the test when it fails. With an optional, it unwraps the value or stops the test.

let customer = try #require(Customer(id: 123))
try #require(engine.parts.first != nil)

Swift 6.0, Xcode 16.0 Apple's page

#expect(throws:)

Checks that code throws (or, with Never.self, does not throw). The returned value is the error that was thrown.

#expect(throws: (any Error).self) { try f() }
let error = #expect(throws: (any Error).self) { try f() }
#expect(throws: Never.self) { try f() }

Swift 6.0, Xcode 16.0 Apple's page

Issue.record

Records a failure unconditionally, for places where no condition fits, such as a guard's else branch.

guard case .electric = engine else {
  Issue.record("Engine is not electric")
  return
}

Swift 6.0, Xcode 16.0 Apple's page

confirmation

Confirms that an event happens while the closure runs. Call the confirmation like a function when the event happens. One time by default.

await confirmation("…") { soldFood in
  FoodTruck.shared.eventHandler = { event in
    if case .soldFood = event { soldFood() }
  }
  await Customer().buy(.soup)
}

Swift 6.0, Xcode 16.0 Apple's page

confirmation(expectedCount: 10...)

expectedCount can be a number or a range with a lower bound, such as 10... (at least ten) or 0..<1000.

await confirmation("…", expectedCount: 10...) { soldFood in
  ...
}

Swift 6.1, Xcode 16.3 Apple's page

withKnownIssue

Marks failures inside the closure as known, so they do not fail the test. isIntermittent: true allows runs that pass.

withKnownIssue("Grill is out of fuel") {
  try FoodTruck.shared.grill.start()
}
withKnownIssue("Grill may need fuel", isIntermittent: true) { ... }

Swift 6.0, Xcode 16.0 Apple's page

.enabled(if:)

Runs the test only when the condition is true; otherwise it is skipped before it starts.

@Test("Ice cream is cold", .enabled(if: Season.current == .summer))
func isCold() async throws { ... }

Swift 6.0, Xcode 16.0 Apple's page

.disabled

Skips the test always, or when the condition is true. The comment shows in the test results.

@Test("Food truck sells burritos", .disabled("We only sell Thai cuisine"))
func sellsBurritos() async throws { ... }

@Suite(.disabled(if: CashRegister.isEmpty))
struct FoodTruckTests { ... }

Swift 6.0, Xcode 16.0 Apple's page

.timeLimit

Fails the test if it runs longer than the limit. Minutes only, at least one minute. On a suite, it applies to each test.

@Test(.timeLimit(.minutes(60)))
func serve100CustomersInOneHour() async { ... }

Swift 6.0, Xcode 16.0 Also needs iOS 16, macOS 13, tvOS 16, watchOS 9. Apple's page

.tags

Labels tests so you can filter and group them. Declare tags in an extension of Tag with @Tag.

extension Tag {
  @Tag static var legallyRequired: Self
}

@Test("Vendor's license is valid", .tags(.legallyRequired))
func licenseValid() { ... }

Swift 6.0, Xcode 16.0 Apple's page

.serialized

Runs the tests of a suite (or the cases of a parameterized test) one at a time instead of in parallel.

@Suite(.serialized)
class RefrigeratorTests {
  @Test func lightComesOn() throws { ... }
  @Test func lightGoesOut() throws { ... }
}

Swift 6.0, Xcode 16.0 Apple's page

.bug

Links a test to a bug report by URL, by ID, or both, with an optional title.

@Test("Food truck engine works", .bug("https://www.example.com/issues/12345"))
func engineWorks() async { ... }

@Test(.bug(id: "12345", "Forgot to buy more napkins"))
func hasNapkins() async { ... }

Swift 6.0, Xcode 16.0 Apple's page

@Test(arguments:)

Runs the test once per value. Each value is a separate test case, so a failure names the input that failed. Cases run in parallel by default.

@Test("All foods available", arguments: Food.allCases)
func foodAvailable(_ food: Food) async throws {
  let foodTruck = FoodTruck(selling: food)
  #expect(await foodTruck.cook(food))
}

Swift 6.0, Xcode 16.0 Apple's page

async tests

Test functions can be async and throwing, and can await code directly.

@Test func priceLookupYieldsExpectedValue() async {
  let mozarellaPrice = await unitPrice(for: .mozarella)
  #expect(mozarellaPrice == 3)
}

Swift 6.0, Xcode 16.0 Apple's page

Test.cancel

Ends a running test early without failing it, the replacement for throw XCTSkip.

if drawer.isEmpty {
  try Test.cancel("Cash register is empty")
}

Swift 6.3, Xcode 26.4 Apple's page

Attachment.record

Attaches a value to the test results, the replacement for XCTAttachment.

let tortilla = Tortilla(diameter: .large)
Attachment.record(tortilla)

Swift 6.2, Xcode 26.0 Apple's page

Sources

Apple's pages, read on 1 November 2026:

How it works

  • •The converter reads your Swift text in the page and rewrites only the patterns Apple's "Migrating a test from XCTest" article describes: imports, the class, setUp and tearDown, test names, the assertion table, XCTUnwrap, XCTFail, skips, XCTExpectFailure, and expectations paired with one wait.
  • •Anything else stays as you wrote it, with a numbered comment on the line and a note below the result that says why. Lines it changed but that behave a little differently (confirmation, Test.cancel, traits) get a note too.
  • •Classes that use XCUIApplication, measure or XCTest metrics are kept as XCTestCase, because Apple writes UI and performance tests with XCTest. A class that mixes those with ordinary tests is split in two in the same file: the ordinary tests move to a new Swift Testing suite, the UI and performance tests stay in the original XCTestCase class, and setUp, tearDown or properties both halves use are copied to each and marked. import XCTest stays whenever something still needs it.
  • •Four converted sample files, with structs, a final class, confirmations, traits, Test.cancel and a kept performance test, were compiled and run with Xcode 27.1 (Swift 6.4) before publishing.
  • •The mapping table and the reference come from Apple's Swift Testing documentation. Version numbers are the ones on each symbol's Apple page, checked on 1 November 2026.

What it does and does not do

  • •It does not compile or run your code, and it does not know your types. Build the result and run the tests once.
  • •Calls spread over several lines, assertions with file: and line:, XCTAssertEqual with accuracy:, and XCTAssertThrowsError with a closure are flagged, not rewritten.
  • •An expectation is converted only when it has exactly one matching wait(for:) or fulfillment(of:) in the same block. confirmation does not wait, so code that fulfills from a later callback needs reworking.
  • •Tests that change stored properties make the suite a final class; it does not rewrite them to use local variables.
  • •It does not add @MainActor. XCTest ran synchronous tests on the main actor and Swift Testing does not, so add it to tests that need the main thread.
  • •UI tests and performance tests are not converted: Apple has no Swift Testing replacement for them. In a mixed class they stay behind in the XCTestCase class while the other tests move out.

FAQ

What is the Swift Testing version of XCTAssertEqual?
#expect(a == b). Apple's table maps every comparison assertion to #expect with the matching operator: XCTAssertNotEqual to !=, XCTAssertGreaterThan to >, XCTAssertIdentical to ===, and so on. For XCTAssertEqual with accuracy: there is no equivalent; Apple points to isApproximatelyEqual() in swift-numerics.
What replaces setUp and tearDown in Swift Testing?
init() and deinit. Swift Testing makes a new instance of the suite for each test, so put setup in init (it may be async and throwing). If you need deinit, Apple says to declare the suite as a class or an actor rather than a struct.
How do I replace XCTestExpectation and wait(for:timeout:)?
Use await confirmation("…") { event in … } and call event() where you called fulfill(). expectedFulfillmentCount becomes expectedCount:, and assertForOverFulfill = false becomes a range such as 10.... A confirmation does not wait: the event must happen before the closure returns, so await the work inside it.
Can I use Swift Testing for UI tests or performance tests?
No. Apple's Xcode documentation says Xcode keeps XCTest for UI tests, and performance tests use XCTest's measure(_:). Keep those tests in an XCTestCase class; they can sit in the same target, and even the same file, as Swift Testing tests. If one class mixes them with ordinary tests, the converter here splits it: the ordinary tests become a Swift Testing suite and the UI or performance tests stay in the XCTestCase class.
What replaces XCTSkip and XCTSkipIf?
For a condition known before the test starts, use the .disabled(if:) or .enabled(if:) trait on the test or suite. To stop a test that is already running without failing it, Apple's replacement is try Test.cancel("…"), which needs Swift 6.3 and Xcode 26.4 or later.
Do I have to convert all my tests at once?
No. A file can import both XCTest and Testing, and a target can hold both kinds of tests. You can move tests over a few at a time. Apple's article also describes interoperability: with a Swift 6.4 toolchain and swift-tools-version 6.4, an XCTest assertion that fails inside a Swift Testing test fails that test (with an older tools version it is a warning, and before Swift 6.4 it is ignored), so convert the assertions rather than relying on it.

Guides that use this tool

Related