Highlight
Swift Testing is a new open-source testing package introduced this year. It can coexist with XCTest in the same target, and Xcode 16 selects it by default when you create a new test target.
Core Content
The most annoying part of writing tests isn’t the tests themselves—it’s XCTest’s boilerplate. Every test must inherit from XCTestCase, method names must start with test, you have to memorize a pile of XCTAssertXxx function signatures, and setUp/tearDown lifecycles need careful handling. None of that is “test logic”—it’s framework overhead.
Swift Testing minimizes that framework noise. You only need two things: @Test to mark functions and #expect for assertions. No class inheritance, no dozen assertion functions to remember—#expect accepts ordinary Swift expressions and automatically captures subexpression values on failure. When creating a new test target in Xcode 16, Swift Testing is already the default option.
The framework’s four building blocks are: @Test marks test functions, #expect/#require perform assertions, Traits customize test behavior, and Suites organize test structure. Each test function creates an independent instance when run in a Suite, avoiding the shared-state problems common in XCTestCase. The entire framework uses an open development process—community members can contribute and propose changes on GitHub.
Detailed Content
Write Your First Test
Import the Testing module, mark a global function with @Test, and assert with #expect (01:54):
import Testing
@testable import DestinationVideo
@Test func videoMetadata() {
let video = Video(fileName: "By the Lake.mov")
let expectedMetadata = Metadata(duration: .seconds(90))
#expect(video.metadata == expectedMetadata)
}
Key points:
import Testingbrings in the framework;@testable importis a Swift language feature for accessing internal members@Testcan annotate global functions or methods on types, and supportsasync,throws, and global actor isolation#expectaccepts ordinary expressions and operators, automatically capturing source and both sides’ values on failure
#require: Early Test Termination
#require is a strict version of #expect for optional unwrapping or scenarios where you need to terminate the test early (02:35):
@Test(.tags(.formatting)) func formattedDuration() async throws {
let videoLibrary = try await VideoLibrary()
let video = try #require(await videoLibrary.video(named: "By the Lake"))
#expect(video.formattedDuration == "0m 19s")
}
Key points:
#requireneedstry; it throws and terminates the test when the expression is false or the optional is nil- Best for scenarios where later code depends on earlier results—continuing makes no sense when the value doesn’t exist
Traits: Customize Test Behavior
Traits are the third building block—they add descriptive info, control run conditions, and modify behavior (06:06):
@Test(.disabled("Due to a known crash"),
.bug("example.org/bugs/1234", "Program crashes at <symbol>"))
func example() {
// ...
}
Key points:
.disabled(...)disables a test but keeps the code compiling—safer than commenting out the function.bug(...)links to issue tracking; clickable directly from the Test Report in Xcode 16.enabled(if:)controls run conditions; marks as skipped when the condition isn’t met.tags(...)labels tests for grouping across files and targets; filter and run by tag in Test Navigator
Suite: Organize Test Structure
Wrap test functions in a struct to form a Suite (07:18):
@Suite(.tags(.formatting))
struct MetadataPresentation {
let video = Video(fileName: "By the Lake.mov")
@Test func rating() async throws {
#expect(video.contentRating == "G")
}
@Test func formattedDuration() async throws {
let videoLibrary = try await VideoLibrary()
let video = try #require(await videoLibrary.video(named: "By the Lake"))
#expect(video.formattedDuration == "0m 19s")
}
}
Key points:
- Suites encourage structs—value semantics avoid shared state; each
@Testmethod creates an independent Suite instance - Stored properties replace traditional
setUp;initreplacessetUp,deinitreplacestearDown(deinit requires class or actor) @Suiteattributes can specify display name and traits; child Suites and inner tests inherit them
Parameterized Tests
Merge repetitive test functions into one parameterized test (14:07):
@Test("Number of mentioned continents", arguments: [
"A Beach",
"By the Lake",
"Camping in the Woods",
"The Rolling Hills",
"Ocean Breeze",
"Patagonia Lake",
"Scotland Coast",
"China Paddy Field",
])
func mentionedContinentCounts(videoName: String) async throws {
let videoLibrary = try await VideoLibrary()
let video = try #require(await videoLibrary.video(named: videoName))
#expect(!video.mentionedContinents.isEmpty)
#expect(video.mentionedContinents.count <= 3)
}
Key points:
- Add parameters to the function signature;
@Test’sarguments:specifies input values - Each parameter value appears as a separate test entry in Test Navigator—rerun and debug individually
- Better than for…in loops: each parameter’s result is visible independently, supports parallel execution, and you can rerun only failed parameters
Relationship with XCTest
Both can coexist in the same test target (17:36). UI automation (XCUIApplication) and performance testing (XCTMetric) still require XCTest. Migration tips: multiple structurally similar XCTest methods can merge into one parameterized test; an XCTestCase with only one test method can migrate to a global @Test function. Don’t call XCTAssert functions in Swift Testing tests, and don’t call #expect in XCTestCase.
Core Takeaways
-
Use parameterized tests to eliminate duplicate test code: Many “same logic, different input” test functions in a project are a maintenance burden. Merge them into parameterized tests—adding a new test case is just one line in the arguments array. How to start: find tests distinguished by suffix in function names (e.g.,
testVideoA,testVideoB) and extract shared logic into a parameterized function. -
Use .tags instead of file directories to organize tests: The traditional approach groups tests by folder, but cross-module relationships are hard to express.
.tagsmarks tests across files and targets; filter and run by tag in Test Navigator, and see correlated insights when multiple tests under the same tag fail in Test Report. How to start: define a Tag for each core feature/subsystem and gradually tag existing tests. -
Use .disabled + .bug instead of commented-out tests: Commented-out tests don’t participate in compilation—the code may have rotted.
.disabledkeeps code always compiling;.buglinks to issue URLs for traceable disable reasons. How to start: search for all commented-out test functions and replace with.disabled("reason").bug("link"). -
Use Swift Testing directly for new projects, introduce gradually in old ones: Both frameworks can coexist in the same target without creating a new one. Add new
@Testfunctions alongside existing XCTestCase to experience#expect’s diagnostics, then migrate gradually once the team adapts. How to start: import Testing in an existing test target and write one new@Testfunction to run through the flow.
Related Sessions
- Go further with Swift Testing — Advanced parameterized testing, tags, and more trait details
- Migrate a test from XCTest to Swift Testing — Step-by-step demo of migrating existing XCTest tests to Swift Testing
- What’s new in Xcode 16 — Overview of all new Xcode 16 features, including Swift Testing IDE integration
- Extend your Xcode Cloud workflows — How Xcode Cloud adapts to the new testing framework
Comments
GitHub Issues · utterances