Key takeaways
- Swift Testing is Apple's replacement for XCTest unit and integration tests, shipping with Xcode 16 and Swift 6. New iOS projects should default to it.
- It runs tests in parallel by default. This surfaces shared-state bugs that XCTest's serial execution hid for years.
- Swift Testing does not cover UI testing or performance testing. XCUITest and XCTMetric stay in XCTest, which is the actual limit on how far the migration goes.
The Swift Testing framework is Apple's modern test framework for Swift code, introduced at WWDC 2024 and shipping with Xcode 16 and Swift 6. It uses macros instead of class inheritance, runs tests in parallel by default, and has two assertion macros (#expect and #require) instead of XCTest's 40+ XCTAssert* variants.
XCTest isn't deprecated. It still ships, still works, and still hosts UI automation and performance tests. But every Apple sample project since WWDC 2024 uses Swift Testing for Swift code, and the new Swift Testing framework replaces XCTest for most new iOS projects.
This guide covers what changed, how to migrate, and where the framework's boundaries still leave teams reaching for XCTest or a third-party tool.
What changed from XCTest
Five things differ fundamentally.
1. Tests are functions, not class methods. No more inheriting from XCTestCase. No more test prefix on function names.
// XCTest
import XCTest
final class UserModelTests: XCTestCase {
func testUserName() {
let user = User(name: "Alice")
XCTAssertEqual(user.name, "Alice")
}
}
// Swift Testing
import Testing
@Test func userName() {
let user = User(name: "Alice")
#expect(user.name == "Alice")
}β
2. Two assertion macros replace 40+ XCTAssert functions. #expect is a soft assertion (test continues on failure). #require is a hard assertion (test stops, used for unwrapping optionals).
@Test func parseResponse() throws {
let data = try #require(response.data) // replaces XCTUnwrap
let user = try JSONDecoder().decode(User.self, from: data)
#expect(user.id == "123")
#expect(user.email.contains("@"))
}β
3. Suites are types, not class hierarchies. Any struct, class, or actor can be a suite. Apple recommends struct unless you need deinit for cleanup.
@Suite("Checkout flow tests")
struct CheckoutTests {
let viewModel: CheckoutViewModel
init() {
viewModel = CheckoutViewModel(api: MockAPI())
}
@Test func cartTotalUpdates() {
viewModel.addItem(price: 100)
#expect(viewModel.total == 100)
}
}β
4. init and deinit replace setUp and tearDown. A fresh instance gets created for every test, so state never leaks between tests by default.
5. Parallel by default. Tests run concurrently using Swift Concurrency. To force serial execution on a specific suite, use .serialized as a trait.
Parallel by default surfaces real bugs
This is the change that breaks the most existing test suites in migration.
XCTest ran tests serially within a process. Tests that shared a global database, a singleton, or a static cache passed because they ran one at a time. Swift Testing runs them simultaneously. Hidden race conditions that lived in test code for years suddenly produce flaky failures.
The fix isn't to mark everything .serialized. The fix is to actually isolate test state, which is what the tests should have been doing the whole time. Per-test fixtures, no global state, dependency injection.
For mobile teams, this means migration time depends on how clean the existing test suite already is. A suite with proper dependency injection and no singletons migrates fast. A suite that mutates UserDefaults.standard in 200 places will fight you.
Traits replace test plan configuration
XCTest configured tests through class hierarchies, naming conventions, and test plans. Swift Testing uses traits applied directly to the test or suite.
Tags are the biggest organizational improvement. You can tag a test with multiple tags, filter in the Test Navigator, and run only tagged subsets in CI. That replaces the awkward XCTest pattern of putting "Slow" or "Regression" in class names and hoping CI knew to skip them.
Parameterized tests are built in
XCTest had no native parameterization. Teams worked around it with loops inside test methods or by generating tests with code. Swift Testing makes it a first-class feature.
@Test(arguments: ["alice@example.com", "bob@test.org", "carol+tag@gmail.com"])
func validEmailsParse(email: String) {
let parsed = EmailParser.parse(email)
#expect(parsed.isValid)
}β
This produces three separate test results, one per argument, all running in parallel. Failure messages identify which argument failed. Re-running a single argument is a one-click operation in Xcode.
For boundary testing (every supported country code, every device class, every locale), this cuts test code by 70% or more.
Setup with init, cleanup with deinit
Setup and teardown move to standard Swift initialization and deinitialization.
@Suite struct DatabaseTests {
let db: Database
init() async throws {
db = try await Database.inMemory()
try await db.migrate()
}
// deinit runs only on class or actor, not struct
@Test func userInsert() async throws {
let user = User(name: "Test")
try await db.insert(user)
#expect(try await db.count(of: User.self) == 1)
}
}β
The benefit is regular Swift code, not framework-specific lifecycle methods. The catch is that deinit only works on class or actor, not struct. Apple recommends starting with struct and switching to final class when cleanup is genuinely needed.
What stays in XCTest
This is the part most blog posts gloss over. Swift Testing does not cover:
UI tests (XCUITest). XCUIApplication, XCUIElement, XCUIElementQuery, the entire UI testing API, are XCTest-only. Swift Testing does not provide a replacement. XCUITest still owns UI testing, but the Swift Testing framework is the unit-test layer in modern iOS.
Performance tests (XCTMetric). measure(metrics:), performance baselines, and XCTMetric are XCTest-only. No Swift Testing equivalent yet.
Objective C tests. Swift Testing is Swift-only. Mixed Obj-C / Swift codebases keep XCTest for the Obj-C side.
The two frameworks coexist in the same test target, so this isn't a blocker. New unit tests go in Swift Testing. UI tests stay in XCTest. Performance tests stay in XCTest. Most teams end up running both indefinitely.
The UI testing gap
The biggest practical impact of Swift Testing isn't on the unit side. It's the pressure it puts on the UI testing layer.
After migrating unit tests to Swift Testing's cleaner syntax, teams look at their XCUITest files and realize they're staring at the last sloppy corner of the test suite. XCUITest has the same problems it had in 2018: selector brittleness, accessibility identifier maintenance, flaky timing, no parameterization, slow execution on real devices.
Apple hasn't said when (or if) Swift Testing will extend to UI testing. Until then, teams have three options:
- Stay on XCUITest. Works, free, supported. Pays the brittleness tax forever.
- Move UI tests to Appium or Maestro. Cross-platform, but adds dependencies and a different testing model.
- Move UI tests to a Vision AI tool. Tests written as plain language commands, executed against real devices, no selectors.
If you're already using the Swift Testing framework for unit tests, your UI layer needs a different tool. The split is natural: Swift Testing for fast, deterministic logic tests, and a UI testing layer that survives design changes without rewrites.
Drizz fits the UI testing slot specifically because Swift Testing didn't fill it. Tests written as Tap "Checkout", Validate "Order placed" is visible run against real iOS devices, find elements visually instead of by accessibility ID, and don't break when the design team renames a button. The unit-test layer stays in Swift Testing where it belongs. The UI layer stops being the part of the suite everyone dreads touching.
How to migrate
Apple's official guidance is incremental. Don't do a big-bang rewrite. The migration pattern that works:
Phase 1: New tests in Swift Testing. Any new feature, any new bug fix, write the test in Swift Testing. Both frameworks run in the same test target.
Phase 2: Migrate by file. When you touch an existing XCTest file for any reason (bug, refactor, feature), migrate the whole file to Swift Testing. Avoid mixing both in one file.
Phase 3: Migrate by suite as you have time. Backlog migrations for sprints where capacity allows. Don't block feature work.
Phase 4: UI tests last (or never). XCUITest stays until Apple ships a replacement, or your team replaces it with something else for unrelated reasons.
The migration math: a 200-test suite with proper dependency injection takes 1 to 2 sprints. A 1,000-test suite with shared state and singletons takes 2 to 4 sprints, mostly fixing the parallelization issues that Swift Testing exposes.
What's new in WWDC 2025
Two additions worth knowing:
Custom attachments. Tests can attach arbitrary Attachable data to results: screenshots on failure, generated input files, diagnostic logs. Attachments surface in Xcode's test reporter and CI artifacts.
Exit tests. Verify code that's expected to terminate under specific conditions (fatal errors, preconditions, exit() calls). Previously impossible without process wrapping.
According to Apple's Swift Testing documentation, the framework is open source under the swift-testing repository on GitHub, runs on Linux and Windows, and is the recommended default for all new Swift code testing.
When to stay on XCTest
A few cases where holding off makes sense:
- Codebase is mostly Objective-C with Swift tests as a minority
- Test target depends heavily on XCTMetric for performance tracking
- Team can't upgrade to Xcode 16 / Swift 6 due to CI or third-party constraints
- Existing XCTest suite is stable, well-organized, and there's no driver for change
For everyone else, Swift Testing is the default starting point in 2026.
FAQ
What is the Swift Testing framework?
Apple's modern test framework for Swift code, introduced at WWDC 2024. It uses macros, runs tests in parallel by default, and ships with Xcode 16.
Should I use Swift Testing or XCTest in 2026?
Swift Testing for all new unit and integration tests. XCTest stays for UI tests (XCUITest), performance tests (XCTMetric), and Objective-C code.
How is Swift Testing different from XCTest?
Tests are functions instead of class methods, two macros replace 40+ assertions, parallel by default, and parameterized tests are built in.
Does Swift Testing replace XCUITest for UI tests?
No. Swift Testing covers unit and integration tests only. UI automation stays in XCTest with XCUITest until Apple ships a replacement.
Can I use Swift Testing and XCTest in the same project?
Yes. Both frameworks coexist in the same test target. You can even mix them in the same file, though Apple recommends keeping them separate.
How long does migrating from XCTest to Swift Testing take?
A clean 200-test suite migrates in 1 to 2 sprints. Larger suites with shared state take longer because parallel execution surfaces hidden race conditions.
β


