Swift Testing: From XCTest to Parameterized and Async Tests
A practical guide to Swift Testing assertions, requirements, parameterized cases, suites, traits, and asynchronous code.
Swift Testing gives Swift code its own test declarations and assertion macros. XCTest remains available, and both frameworks can live in the same test target. The useful question is not whether one framework can express an equality check; both can. It is how you express a group of cases, a failed precondition, or asynchronous work without hiding what the test actually checks. Apple documents Swift Testing for new unit and integration tests and XCTest for UI and performance tests in its XCTest overview.
Updated September 2026 for Swift 6.4 interoperability.
Swift Testing became part of the Swift toolchain with Swift 6 on September 17, 2024. This article also notes an interoperability change from Swift 6.4 in September 2026, so its examples describe more than the initial release.
The examples below belong in a test target. Start with a small function that has an unambiguous result:
func isEven(_ number: Int) -> Bool {
number.isMultiple(of: 2)
}
The same test in two frameworks
In XCTest, a test method belongs to an XCTestCase subclass. The method name starts with test, and an assertion records a failure if the actual and expected values differ:
import XCTest
final class NumberTests: XCTestCase {
func testFourIsEven() {
XCTAssertEqual(isEven(4), true)
}
}
In Swift Testing, the @Test attribute marks a function. The #expect macro evaluates a Boolean expression and records an issue when it is false:
import Testing
@Test func fourIsEven() {
#expect(isEven(4) == true)
}
Neither test proves that every integer is handled correctly. Each checks one input. The framework changes the test syntax and reporting, not the scope of the assertion. XCTest and Swift Testing tests can coexist in one target. Swift 6.4 also added support for using #expect within an XCTest method or XCTAssert within a Swift Testing test; that interoperability depends on the toolchain version. The examples here use each framework's own style to make the comparison clear. See the Swift 6.4 release notes and Migrating a test from XCTest.
Check a condition or require a value
#expect records an issue and continues running the test. That is useful when later, independent checks can still produce meaningful results. #require also checks a condition, but throws on failure. It can unwrap an optional, avoiding a force unwrap followed by a misleading secondary failure.
Suppose the function under test returns the first even number, if one exists:
func firstEven(in numbers: [Int]) -> Int? {
numbers.first { $0.isMultiple(of: 2) }
}
@Test func findsFirstEven() throws {
let result = try #require(firstEven(in: [1, 4, 6]))
#expect(result == 4)
}
If the array has no even number, #require records the missing value and stops this test. It also gives result the non-optional type Int. The test is marked throws because the requirement can throw. Apple's expectations documentation describes this distinction.
Run the same rule against several values
A parameterized test creates a separate case for each input. This is a good fit when the setup and assertion stay the same:
@Test(arguments: [0, 2, 4, 6])
func acceptsEvenNumber(_ number: Int) {
#expect(isEven(number) == true)
}
The runner reports the input associated with a failing case. For input/output pairs, pass a zipped collection:
@Test(arguments: zip([2, 3, 4], [true, false, true]))
func matchesExpectedParity(_ number: Int, _ expected: Bool) {
#expect(isEven(number) == expected)
}
Using two separate collections instead of zip would produce every combination: three numbers times three Boolean values, or nine cases. zip produces the three intended pairs. This behavior is specified in Apple's parameterized testing guide.
Parameterized cases run in parallel by default. In particular, do not make their result depend on a shared mutable counter or on the order in which inputs happen to run.
Organize tests with suites and traits
A Swift type containing @Test functions is recognized as a suite without an explicit @Suite attribute. Add @Suite when you need a display name or traits. A tag can classify related tests across multiple files; a tag applied to a suite is inherited by its tests:
extension Tag {
@Tag static var arithmetic: Self
}
@Suite("Parity rules", .tags(.arithmetic))
struct ParityTests {
@Test(arguments: [2, 4, 6])
func evenInput(_ number: Int) {
#expect(isEven(number) == true)
}
}
Traits can also control conditions and time limits. Tags provide a way to filter related tests without moving them into one source file. They do not change an assertion's meaning. See Apple's guides to suites and tags.
Test async work and callbacks
A test function can be async and use ordinary await. This example creates a fresh actor for the test, calls an asynchronous actor-isolated method, and checks its returned value:
actor Counter {
private var value = 0
func increment() -> Int {
value += 1
return value
}
}
@Test func counterIncrements() async {
let counter = Counter()
let result = await counter.increment()
#expect(result == 1)
}
Callbacks are different: there may be no value to await. Swift Testing provides confirmation to check that an event occurs while the supplied operation runs:
func callNow(_ completion: () -> Void) {
completion()
}
@Test func invokesCompletion() async {
await confirmation("Completion is called") { confirm in
callNow { confirm() }
}
}
This callback is synchronous. confirmation expects the event before its operation returns; it is not a sleep or an unbounded wait for some later callback. For an API that already offers an async alternative, awaiting that API is simpler. Apple's asynchronous testing guide covers both approaches.
Swift Testing can express these unit and integration tests without changing production code. Keep the test focused on an observable result, use #require when a missing prerequisite prevents further checks, and use parameterization only when every case follows the same rule. For UI automation and performance measurements, continue using XCTest.