XCTest ties every test to a class and a test-prefixed method name, and its assertions halt the method on first failure. That worked fine for years. It stays verbose for the common case of "run this logic against a handful of inputs."
#The old way
import XCTest
final class PriceFormatterTests: XCTestCase {
func testFormatsWholeDollars() {
let formatter = PriceFormatter()
XCTAssertEqual(formatter.format(cents: 500), "$5.00")
}
func testFormatsFractionalCents() {
let formatter = PriceFormatter()
XCTAssertEqual(formatter.format(cents: 599), "$5.99")
}
}
#The new way
import Testing
struct PriceFormatterTests {
@Test("Formats cents as dollars", arguments: [
(500, "$5.00"),
(599, "$5.99"),
(0, "$0.00")
])
func formatsCents(cents: Int, expected: String) {
let formatter = PriceFormatter()
#expect(formatter.format(cents: cents) == expected)
}
@Test
func throwsOnNegativeCents() {
let formatter = PriceFormatter()
#expect(throws: PriceError.self) {
try formatter.validate(cents: -100)
}
}
}
#Why it matters
arguments:gives real parameterised tests. One test body runs once per input, each reported as its own pass/fail, instead of one method looping over cases#expectkeeps evaluating past a failed check within the same test instead of aborting likeXCTAssert, so one run surfaces every failure in the body rather than stopping at the first- Plain structs replace
XCTestCasesubclassing. Notest-prefixed method names, no inherited setup/teardown boilerplate #expect(throws:)checks an error type or a specific error value inline, without a do/catch scaffold
#Gotcha
Swift Testing runs test methods within a suite concurrently by default, unlike XCTest's serial-by-default execution. Tests that share mutable state (a singleton, a static var) can interfere with each other in ways that only show up intermittently. Mark a suite @Suite(.serialized) if its tests can't run in parallel. Swift Testing and XCTest coexist in the same target, but UI testing (XCUIApplication) and performance tests (XCTMetric) still require XCTest, so Swift Testing doesn't replace those. Check exact API details against current Xcode docs, since the framework is still evolving quickly.