apple / swift-testing

A modern, expressive testing package for Swift
Apache License 2.0
1.47k stars 52 forks source link

confirmation API without async calls #510

Closed mattmassicotte closed 3 days ago

mattmassicotte commented 3 days ago

Description

I'm a beginner! I was trying to migrate over some XCTest-based code, and got tripped up by how confirmation works.

@Test func fails() async {
    // ERROR: Confirmation was confirmed 0 times, but expected to be confirmed 1 time
    await confirmation() { completion in
        _ = Task {
            completion()
        }
    }
}

@Test func succeeds() async {
    await confirmation() { completion in
        await Task {
            completion()
        }.value
    }
}

I get it, and it makes sense. It's just different from how XCTests's fulfillment API works.

Expected behavior

I wonder if it would be possible to maybe make an non-async overload that waits for the expected number of confirmations?

Actual behavior

No response

Steps to reproduce

No response

swift-testing version/commit hash

No response

Swift & OS version (output of swift --version ; uname -a)

swift-driver version: 1.110 Apple Swift version 6.0 (swiftlang-6.0.0.4.52 clang-1600.0.21.1.3)
Target: arm64-apple-macosx14.0
Darwin Matthews-MBP-2.localdomain 23.5.0 Darwin Kernel Version 23.5.0: Wed May  1 20:12:58 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6000 arm64
grynspan commented 3 days ago

It is intentional that the function does not block the caller waiting for work to complete. Such work is generally best handled using Swift concurrency instead. Watch Go further with Swift Testing where we talk about this sort of pattern.

mattmassicotte commented 3 days ago

I agree! But as it stands, this API is actually quite easy to use wrong like I just did.

I think an overload allow this to just work transparently. What would the downside be?

public func confirmation<R>(_ comment: Testing.Comment? = nil, expectedCount: Int = 1, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, column: Int = #column, _ body: (Testing.Confirmation) throws -> R) async rethrows -> R {
    try await confirmation(comment, expectedCount: expectedCount, fileID: fileID, filePath: filePath, line: line, column: column) { thing in
        try await withCheckedThrowingContinuation { continuation in
            let result = Result(catching: { try body(thing) })

            continuation.resume(with: result)
        }
    }
}

Edit: thinking on this more, I guess that won't work outside of some trivial cases. Oh well!

grynspan commented 3 days ago

In the video, we discuss this exact pattern. 🙂

mattmassicotte commented 3 days ago

Next you're going to ask me to understand all the tools I try to use! 😉

grynspan commented 3 days ago

Yes, but only out of love. 🙃