← All shorts
69Xcode 16+2 min read

Swift Testing: @Test, #expect, and Parameterised Tests

Swift Testing replaces XCTest's class-and-assertion ceremony with free functions, macros, and native parameterisation.

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
  • #expect keeps evaluating past a failed check within the same test instead of aborting like XCTAssert, so one run surfaces every failure in the body rather than stopping at the first
  • Plain structs replace XCTestCase subclassing. No test-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.

swift-testingtestingxcode

Related shorts