Of course, we're still far from getting async/await into release (as well as actors, structured concurrency etc), but there is one thing I would love to have early on just to try on what we have already — ability to test async code.
Sure, async syntax might change, and therefore it's not reasonable to update XCTest right away, but what about a temporary hack to enable us testing stuff as usual?
I tried this, but got an expected compile error:
func asyncTest<T>(closure: () async throws -> T) throws -> T {
var result: Result<T, Error>
let group = DispatchGroup()
group.enter()
_ = Task.runDetached {
do {
result = .success(try await closure()) // Mutation of captured var 'result' in concurrently-executing code
} catch {
result = .failure(error) // Mutation of captured var 'result' in concurrently-executing code
}
group.leave()
}
group.wait()
switch result {
case let .success(res): return res
case let .failure(err): throw err
}
}
Of course, this check is reasonable, but in this particular case I know what I'm doing (or do I? vsauce.mp3).
Is there any way of force-syncing an async piece of code?
Hm. TBH I didn't think of using runAsyncAndBlock for that purpose... Maybe because it's not throwing (_runAsyncMain is though). I'm afraid it might break XCTest runtime. Will try, however. Thank you.
What I'm saying is that XCTest already has tools to "wait until async function is completed (or timeout reached)", so if you have a completion based API (that you will switch to async/await), you can already test that just like how you would test async/await the same way OP wrote their example code but instead of DispatchGroup, they should just use the proper XCTest API that has been existed for a long time.
Nothing from XCTest is automatically imported in a way that's useful for async APIs. So in the short term, using the async context workaround with runAsyncAndBlock works fine.
OK so _runAsyncMain doesn't work at all in tests, runAsyncAndBlock only allows non-throwing closures, force trying functions isn't good, and XCTAssertNoThrow doesn't work with async functions, obviously. Sigh.
IIRC calling XCTFail on another thread can cause some other issues, so you will have to go with your previous solution. (But with XCTExpectation instead of DispatchGroup)
(But I still don't understand how you "kinda got it working eventually" when you almost had a working solution and I told you exactly how to make it work)
So something like this:
var result: Result<T, Error>?
let expectation = self.expectation(description: expectationDescription ?? "Async operation")
Task.runDetached {
do {
result = .success(try await closure())
} catch {
result = .failure(error)
}
expectation.fulfill()
}
self.wait(for: [expectation], timeout: timeout)
switch result {
case let .success(result)
return result
case let .failure(error):
throw error
case nil:
throw "Async function timed out"
}
Have you tried it? It won't compile. Maybe wrapping Swift.Result in a class will work, but still it's extremely ugly, not to mention I'm too lazy to create async versions of assertion functions.
sorry to resurrect this thread, but the solutions here don't seem to work any more:
runAsyncAndBlock is gone
detach executes the contents on an arbitrary thread, so it's unsafe to use XCTAssert within it I think?
It's now an error to mutate a captured variable from concurrent code (there's lots of solutions once you're already in an async context, but I don't see how to get data out of detach at the moment without writing my own unsafe locking code?
It really feels like XCTest needs an update to handle the new async stuff. Ideally you could just write async func testBlah() { ... XCTAssert(...) } and have XCTest itself manage all the details (using a task-local for XCTAssert to contribute to the results of the correct test, for example).
It's an error when you are trying to capture the variable like this:
let result: Result<T, Error>
It works when you capture it like this (because this gets initialized to nil):
var result: Result<T, Error>?
Waiting for expectations makes sure that there won't be any concurrent accesses (if you only access it before fulfilling the expectation and continueOnFailure is set to false)
I've just found out about this issue myself. This is bothersome.
I have solved this issue for now by disabling async tests, I can't merge to main something that may or may not work in future without a previous notice.
I started working on it myself (out of curiosity, with no intent of creating a PR; either way, I'm sure it's already in work somewhere inside Apple), but I have no clue how to integrate updated XCTest into toolchain (Xcode?)
Xcode uses the Objective-C version of XCTest that has been incorporated as part of Xcode that pre-dates Swift and can be used with the C-style languages as well as Swift. The XCTest that is part of the Swift repository is a version of XCTest for Swift only that is intended for Linux, Windows, FreeBSD, etc., so that testing on those platforms mimics the XCTest package on MacOS as much as possible, given that the Objective-C run-time is not available on those platforms. I think you can use that version on Darwin, but, usage has to be outside of the Xcode environment, in particular, xcodebuild does not use it, as far as I know.