The Swift Testing Framework

Reviewed & published by Brayan K

Apple's modern testing framework (Xcode 16) replaces XCTest's classes and assertions with macros. Learn import Testing, @Test functions, #expect and #require, parameterized tests with arguments:, @Suite, tags, and async tests.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ @Test and #expect

Import Testing, mark a function @Test, and check results with #expect. No base class and no test prefix required.

import Testing

// Code under test:
func add(_ a: Int, _ b: Int) -> Int { a + b }

// @Test marks a function as a test. No subclass, no "test" prefix needed.
@Test func addsTwoNumbers() {
    #expect(add(2, 3) == 5)     // #expect replaces XCTAssert
}

@Test("addition is commutative")   // tests can have a display name
func commutative() {
    #expect(add(1, 4) == add(4, 1))
}

2️⃣ #require and Expected Errors

#require throws to stop a test early — ideal for unwrapping. #expect(throws:) verifies that code throws the error you expect.

import Testing

// #require stops the test by THROWING when its condition fails,
// so it is perfect for safely unwrapping optionals.
@Test func findsFirstEven() throws {
    let numbers = [1, 3, 4, 7]
    let firstEven = try #require(numbers.first { $0 % 2 == 0 })
    // If nothing matched, #require would throw and stop here.
    #expect(firstEven == 4)
}

// #expect(throws:) checks that code throws the expected error.
enum BankError: Error { case insufficientFunds }
func withdraw(_ amount: Int, from balance: Int) throws {
    if amount > balance { throw BankError.insufficientFunds }
}

@Test func rejectsOverdraft() {
    #expect(throws: BankError.insufficientFunds) {
        try withdraw(100, from: 10)
    }
}

3️⃣ Parameterized Tests

Pass a collection to @Test(arguments:) and the function runs once per input, reporting each case independently — far cleaner than a hand-written loop.

import Testing

func isEven(_ n: Int) -> Bool { n % 2 == 0 }

// One test, MANY inputs. It runs once per argument and reports each.
@Test(arguments: [2, 4, 8, 100])
func evensAreEven(_ value: Int) {
    #expect(isEven(value))
}

// Pair up inputs and expected outputs with zip.
@Test(arguments: zip([1, 2, 3], [2, 4, 6]))
func doubles(input: Int, expected: Int) {
    #expect(input * 2 == expected)
}

Your turn. Fill in the import, the test attribute, and the assertion macro.

// 🎯 YOUR TURN — fill in each ___ then run it in Xcode 16.
___ Testing                          // 👉 the module to import

func square(_ n: Int) -> Int { n * n }

___ func squaresCorrectly() {        // 👉 the attribute that marks a test
    ___(square(4) == 16)             // 👉 the assertion macro
}

// ✅ Once filled in, the test passes because square(4) == 16.

4️⃣ Suites, Tags, and Async

@Suite groups related tests; traits like .tags(...) add filterable metadata; and any @Test can be async to await directly.

import Testing

// @Suite groups related tests. Async tests just mark the function async.
@Suite("String utilities")
struct StringTests {

    @Test func reversesText() {
        #expect(String("abc".reversed()) == "cba")
    }

    // An async test awaits directly — no expectations boilerplate.
    @Test func loadsValue() async {
        let value = await fetchSeven()
        #expect(value == 7)
    }

    // A tag trait helps you filter and organize runs.
    @Test(.tags(.fast)) func quickCheck() {
        #expect(1 + 1 == 2)
    }
}

extension Tag { @Tag static var fast: Self }
func fetchSeven() async -> Int { 7 }

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Importimport Testingthe framework
Test@Test func x()no subclass
Assert#expect(a == b)keeps going
Unwrap/stoptry #require(opt)throws on fail
Parameterized@Test(arguments: ...)one per input
Group@Suite struct ...collection

Mini-Challenge: Parameterized Validation

Write a parameterized test plus a negative case for an age validator.

// 🎯 MINI-CHALLENGE: parameterized validation
// Given:  func isValidAge(_ a: Int) -> Bool { a >= 0 && a <= 120 }
// 1. import Testing
// 2. Write a parameterized @Test using arguments: [0, 30, 120]
//    that takes one Int parameter and #expects isValidAge is true.
// 3. Add a second @Test that #expects isValidAge(-5) is false.
//
// ✅ All cases should pass.

// your code here

Practice quiz

Which module do you import to use the Swift Testing framework?

  • Testing
  • SwiftTest
  • Foundation
  • XCTest

Answer: Testing. Swift Testing is provided by the Testing module, so you write import Testing.

Which attribute marks a function as a test in Swift Testing?

  • @TestCase
  • @Check
  • @Test
  • @Spec

Answer: @Test. You annotate a function with the @Test macro to register it as a test.

Which macro replaces XCTAssert for ordinary checks?

  • #check
  • #expect
  • #assert
  • #verify

Answer: #expect. #expect(...) is the core assertion macro in Swift Testing.

What does #require do that #expect does not?

  • Runs faster
  • Logs only
  • Skips the test
  • Stops the test by throwing when the condition fails

Answer: Stops the test by throwing when the condition fails. #require throws and halts the test on failure, so you can safely unwrap or stop early.

How do you run the same test over many inputs?

  • @Test with an arguments: parameter
  • Copy the test
  • @Repeat
  • A for loop only

Answer: @Test with an arguments: parameter. Parameterized tests use @Test(arguments:) to run the function once per input value.

Which attribute groups related tests into a named collection?

  • @Bundle
  • @Suite
  • @Set
  • @Group

Answer: @Suite. @Suite marks a type as a test suite that organizes related @Test functions.

How do you write an asynchronous test in Swift Testing?

  • Use waitForExpectations
  • You cannot
  • Wrap it in DispatchQueue
  • Mark the @Test function async and use await

Answer: Mark the @Test function async and use await. A @Test function can be async, letting you await asynchronous code directly.

How do you express that a test should throw an error?

  • @Throws
  • try?
  • #expect(throws:)
  • XCTAssertThrows

Answer: #expect(throws:). #expect(throws:) verifies that the enclosed expression throws the expected error.

What are traits like .tags(...) used for?

  • Speeding up tests
  • Adding metadata to filter and organize tests
  • Mocking
  • Compiling

Answer: Adding metadata to filter and organize tests. Traits such as tags attach metadata so you can group, filter, and select tests to run.

How does Swift Testing relate to XCTest?

  • It is a modern alternative that can coexist with XCTest
  • It deletes XCTest
  • It is identical to XCTest
  • It only runs on Linux

Answer: It is a modern alternative that can coexist with XCTest. Swift Testing is a newer, macro-based framework that runs alongside existing XCTest tests.

Continue this course

Frequently asked questions

What is the Swift Testing framework?

Swift Testing is Apple's modern testing framework introduced with Xcode 16 and the Swift 6 toolchain. You import the Testing module and mark functions with the @Test macro instead of subclassing XCTestCase. Assertions use the #expect and #require macros rather than the XCTAssert family. It was designed to feel native to Swift, with clearer failure messages, built-in parameterized tests, suites, tags, and first-class async support.

How do @Test and #expect work?

You attach @Test to any function to register it as a test; it does not need to live in a special class or start with the word test. Inside the function, #expect(condition) records a failure with a rich, expression-aware message if the condition is false, but it lets the test continue. You can also give a test a readable display name by writing @Test("description"). This macro-based design makes tests concise and self-describing.

What is the difference between #expect and #require?

Both check a condition, but they handle failure differently. #expect records a failure and keeps going, which is ideal when you want to see every assertion that failed in one run. #require throws an error and stops the test immediately when its condition fails, which is what you want when later code cannot proceed meaningfully, such as after safely unwrapping an optional with try #require(optional). Use #require for preconditions and #expect for ordinary checks.

How do parameterized tests work?

A parameterized test runs the same logic against many inputs without copy-pasting. You write @Test(arguments:) and pass a collection of values, and the function receives one value per run, reporting each case separately. You can pair inputs with expected outputs using zip, so each row is checked independently and a failure tells you exactly which input broke. This replaces hand-written loops and gives you clearer, isolated results.

How is Swift Testing different from XCTest, and can they coexist?

XCTest relies on subclassing XCTestCase, methods named with a test prefix, and the XCTAssert functions, while Swift Testing uses the @Test and #expect macros, suites, tags, and parameterized arguments. Swift Testing offers better diagnostics and a more Swift-native API. Importantly, the two frameworks coexist in the same project, so teams can keep their XCTest tests running while writing new tests in Swift Testing and migrate gradually.