How do XCTestExpectation tests fail: timeouts, inverted expectations, ordering, over-fulfilment and forgetting to wait?

22.9.2 · L5 · Interview

Predict first

Which expectation mistakes fail with their own messages?

// === Package.swift ===
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "Lib", targets: [.target(name: "Lib"), .testTarget(name: "LibTests", dependencies: ["Lib"])])
// === Sources/Lib/Lib.swift ===
public func square(_ x: Int) -> Int { x * x }
// === Tests/LibTests/T.swift ===
import XCTest
@testable import Lib
final class ExpTests: XCTestCase {
    func testNoOverfulfillCheck() async {
        let e = expectation(description: "once"); e.assertForOverFulfill = false
        e.fulfill(); e.fulfill()
        await fulfillment(of: [e], timeout: 1)
    }
    func testOrder() async {
        let a = expectation(description: "A"), b = expectation(description: "B")
        b.fulfill(); a.fulfill()
        await fulfillment(of: [a, b], timeout: 1, enforceOrder: true)
    }
    func testInvertedPasses() async {
        let e = expectation(description: "never"); e.isInverted = true
        await fulfillment(of: [e], timeout: 0.2)
    }
    func testInvertedFails() async {
        let e = expectation(description: "never"); e.isInverted = true
        DispatchQueue.global().async { e.fulfill() }
        await fulfillment(of: [e], timeout: 0.5)
    }
    func testTimeout() async {
        let e = expectation(description: "slow")
        await fulfillment(of: [e], timeout: 0.1)
    }
    func testForgotWait() {
        let e = expectation(description: "unwaited")
        _ = e
    }
}
  • Timeout, inverted fulfilled, wrong order and unwaited fail with specific messages; over-fulfilment is allowed when assertForOverFulfill is false
  • Only timeouts fail
  • All six tests pass

Predict first

What happens when an expectation expected once is fulfilled twice?

// === Package.swift ===
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "Lib", targets: [.target(name: "Lib"), .testTarget(name: "LibTests", dependencies: ["Lib"])])
// === Sources/Lib/Lib.swift ===
public func square(_ x: Int) -> Int { x * x }
// === Tests/LibTests/T.swift ===
import XCTest
final class OverTests: XCTestCase {
    func testOverfulfill() async {
        let e = expectation(description: "once")
        e.fulfill(); e.fulfill()
        await fulfillment(of: [e], timeout: 1)
    }
}
  • An API-violation exception ends the test process
  • The test fails with a normal assertion
  • Nothing; the second call is ignored

Mental model. An expectation is a delivery you are waiting for: late is a timeout, a parcel that must not arrive is inverted, two parcels when one was ordered is over-fulfilment, and the doorbell nobody answers is the unwaited expectation.

Each way an expectation goes wrong has its own message, and they are worth recognising. A timeout reports “Asynchronous wait failed: Exceeded timeout of 0.1 seconds, with unfulfilled expectations” naming the expectation. An inverted expectation passes if it is not fulfilled within the timeout (and, because it must wait the full timeout to be sure, costs that time: 0.2 s in the check), and fails with “Fulfilled inverted expectation” if something fulfils it. fulfillment(of:timeout:enforceOrder: true) fails with “expectation fulfilled in incorrect order” when B was fulfilled before A. An expectation you create but never wait for fails the test with “Failed due to unwaited expectation”.

Over-fulfilment is nastier: calling fulfill() twice on an expectation expected once raises an NSInternalInconsistencyException (“API violation - multiple calls made to -[XCTestExpectation fulfill]”) which, in this check, terminates the whole test process rather than failing one test. Set assertForOverFulfill = false when extra calls are legitimate (a callback that may fire more than once), or expectedFulfillmentCount for exact counts. Use the async await fulfillment(of:timeout:) and not wait(for:) in async code (it blocks the thread), keep timeouts only as long as a real failure needs, and prefer awaiting the result directly when you can (Q444).

Each misuse has its own message, and the over-fulfilment check can be turned off.

Over-fulfilment raises an Objective-C exception, not a regular test failure.

Common traps

  • Fulfilling the same expectation from a callback that can fire repeatedly. The process can abort; set assertForOverFulfill or count.
  • Using long timeouts on inverted expectations. They always wait the full time, so they slow the suite.

They might follow up with

  • How do I count N events? expectedFulfillmentCount.
  • How does this map to Swift Testing? confirmation with a count, or a continuation you simply await.

Related questions

  • 22.9.1 In what order do XCTest’s class and instance setup, teardown blocks and tearDown run, and what does…
  • 22.8.4 How does confirmation count events in Swift Testing, and what can it not do?
  • 22.3.1 How do you test async code: async test functions, expectations, confirmations, actors and the main actor?
  • 22.7.1 Why do tests flake, how do you find and fix them, and what makes a CI test run hermetic?

← In what order do XCTest’s class and instance setup, teardown blocks…   Why does a hand-written spy fail to compile in Swift 6 when a… →