Tips · No.9

Swift Testing: #expect vs #require Explained

What is the difference between #expect and #require in Swift Testing? Learn when to use `try #require` for optional unwrapping and understand the critical differences between soft and hard assertions.

Published
Updated
Reading
1 min read
Tags
Swift Testing
Language
English /阅读中文 →
Contents5 sections
  1. 1. Syntax: The Necessity of try
  2. 2. Core Feature: Unwrapping Optionals
  3. 3. Runtime Behavior: Continue vs. Stop
  4. Summary: Which One to Choose?
  5. Further Reading

TL;DR: In the Swift Testing framework, #expect is a “soft assertion” (the test continues even if it fails), while #require is a “hard assertion” (the test stops immediately upon failure). Additionally, #require serves as the modern replacement for XCTUnwrap, allowing you to safely unwrap optional values within your tests.

#expect and #require are the two foundational macros in the Swift Testing framework (the standard testing library in Swift 6). While they both serve to verify conditions, they differ fundamentally in their error handling mechanisms and intended use cases.

1. Syntax: The Necessity of try

The mechanism behind #require works by throwing an error to halt test execution. Therefore, regardless of whether the expression inside it throws an error itself, you must use the try keyword when calling it.

Swift
@Test func syntaxDemo() throws {
    let value = 10
    
    // ✅ Correct: #expect does not need 'try' (unless the expression itself throws)
    #expect(value == 10) 
    
    // ✅ Correct: #require must always be used with 'try'
    try #require(value > 0)
}

2. Core Feature: Unwrapping Optionals

#require is the perfect modern replacement for XCTest’s XCTUnwrap. It attempts to unwrap an optional value. If the value is nil, the test fails and stops immediately; if successful, it returns the unwrapped non-optional value for subsequent use.

Swift
@Test func unwrappingDemo() throws {
    let user: User? = fetchUser()
    
    // If user is nil, the test terminates here.
    // Equivalent to: guard let user = user else { throw Failure }
    let validUser = try #require(user) 
    
    // Here, validUser is a non-optional type
    #expect(validUser.name == "Fatbobman")
}

3. Runtime Behavior: Continue vs. Stop

This is the most significant logical difference, determining how issues are reported.

#expect (Soft Assertion)

Even if the assertion fails, the test function continues execution. This allows you to collect multiple error messages in a single test run.

Swift
@Test func softAssertion() {
    #expect(1 == 2) // ❌ Fails, error is recorded
    print("This line will be executed") 
    #expect(3 == 4) // ❌ Fails again, second error is recorded
}

Result: The test is marked as failed, and the report shows 2 issues.

#require (Hard Assertion)

Once the assertion fails, an error is thrown, and the test function terminates immediately. This is ideal for scenarios where subsequent code depends on the current condition (like precondition checks).

Swift
@Test func hardAssertion() throws {
    try #require(1 == 2) // ❌ Fails, throws error, and terminates
    print("This line will never be executed")
}

Result: The test is marked as failed, and the report shows only 1 issue.

Summary: Which One to Choose?

Feature#expect#require
Assertion TypeSoft AssertionHard Assertion
Failure BehaviorRecords error, continues executionRecords error, terminates immediately
KeywordUsually doesn’t need tryMust use try
Return ValueNoneReturns unwrapped value (for Optionals)
Use CaseValue verification, state checksPreconditions, Optional unwrapping

Further Reading

  • Tags
  • Swift
  • Testing

Comments