How do XCTest and Swift Testing differ, and when do you still use XCTest?
Predict first
A failing #expect and a failing #require each sit before a print. Which prints run?
// === Package.swift ===
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "Lib", targets: [.target(name: "Lib"), .testTarget(name: "LibTests", dependencies: ["Lib"])])
// === Sources/Lib/Lib.swift ===
public func square(_ x: Int) -> Int { x * x }
// === Tests/LibTests/T.swift ===
import Testing
import Foundation
@testable import Lib
@Test func continuesAfterExpect() {
#expect(1 == 2)
print("AFTER_EXPECT")
}
@Test func stopsAfterRequire() throws {
let value: Int? = nil
let unwrapped = try #require(value)
print("AFTER_REQUIRE", unwrapped)
}
- The print after #expect runs; the one after #require does not
- Both prints run
- Neither print runs
Predict first
Two tests in one suite each increment a stored counter and expect 1. Do they pass, and how is a four-argument test reported?
// === Package.swift ===
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "Lib", targets: [.target(name: "Lib"), .testTarget(name: "LibTests", dependencies: ["Lib"])])
// === Sources/Lib/Lib.swift ===
public func square(_ x: Int) -> Int { x * x }
// === Tests/LibTests/T.swift ===
import Testing
import Foundation
@testable import Lib
struct Fresh {
var counter = 0
@Test mutating func first() { counter += 1; #expect(counter == 1) }
@Test mutating func second() { counter += 1; #expect(counter == 1) }
}
@Test(arguments: [1, 2, 3, 4])
func squares(_ n: Int) { #expect(square(n) == n * n) }
- Both pass because each test gets a fresh instance; the parameterised test reports 4 cases
- The second fails because the counter is shared
- It reports one case
Mental model. XCTest is a big toolbox of specific assertion tools. Swift Testing is one multi-tool that reads the expression you hand it and tells you exactly which part was wrong.
XCTest is the long-standing, Objective-C-rooted framework: you subclass XCTestCase, name methods test…, use setUpWithError and tearDown, and assert with XCTAssertEqual, XCTAssertTrue, XCTUnwrap and XCTAssertThrowsError. Swift Testing (new with Swift 6 and Xcode 16) is a Swift-native library built on macros: a function marked @Test is a test (no naming rule), a type can be a @Suite, and the single #expect(expression) macro captures the expression and its sub-values, so a failure reads “square(3) == 10 → 9 == 10” without dozens of assertion functions. #require is the throwing form that stops the test when it fails (replacing XCTUnwrap and guard-style asserts), whereas a failed #expect records the issue and the test carries on, as the check below shows. Parameterised tests (@Test(arguments:)) run one body for each input and report each case separately, traits (.tags, .enabled(if:), .disabled, .timeLimit, .serialized, .bug) attach metadata and conditions, confirmation replaces expectations for “this event happened N times”, and withKnownIssue marks a failure you know about.
Behavioural differences matter. A suite gets a fresh instance for every test, so stored properties start clean (no setUp needed; use init and deinit), and tests run in parallel by default, which exposes shared state and makes isolation a requirement, not an option (XCTest by default runs methods in a class one after another, and parallelises across processes or classes when enabled). Async and throwing tests are first-class in both. Keep XCTest for UI tests (XCUIApplication), performance tests with XCTMetric (Q430), anything using XCTestExpectation patterns you have not migrated, and codebases that need to support older toolchains. The two coexist in one target, so migrate incrementally and do not rewrite a working suite just for the syntax.
#expect records the issue and continues; #require throws and ends the test at that line.
Every test runs on a new instance of the suite, and each argument is a separate test case.
| XCTest | Swift Testing | |
|---|---|---|
| Declaring a test | subclass XCTestCase, method named test… | @Test on any function, @Suite on a type |
| Assertions | many XCTAssert… functions | #expect and #require macros |
| After a failed check | continues unless continueAfterFailure is false | #expect continues, #require stops |
| Parallelism | serial in a process; parallel across processes when enabled | parallel by default |
| Per-test state | setUp and tearDown on a shared class instance | a fresh instance per test; init and deinit |
| UI and performance tests | yes | no: stay in XCTest |
Common traps
- Using
#expectwhere later lines need the value. The test continues after a failure and then crashes or reports noise; use#require. - Sharing mutable state between tests. Swift Testing runs them in parallel; give every test its own instance and data.
They might follow up with
- How do you run one test body for several inputs?
@Test(arguments:), which reports each argument as its own case. - Can Swift Testing replace UI tests? No; UI tests and performance metrics still use XCTest.
Related questions
- 22.3.1 How do you test async code: async test functions, expectations, confirmations, actors and the main actor?
- 22.1.1 What is the test pyramid for an iOS app, and what does each layer cost and catch?
- 22.5.2 How do you write UI tests that stay stable: accessibility identifiers, page objects, launch arguments…
- 21.2.2 How do signposts and Points of Interest work, and how do you turn a measurement into an XCTest performance…