I have very little experience with async/await, but this seemed like an interesting issue to tackle. I came up with something that is perhaps a bit messy but seems reliable. I ran this ~30,000 times without failure. It avoids the need for yield() or sleep().
class ScreenshotWatcherTests: XCTestCase {
func testBasics() {
let vm = ViewModel()
let startExpectation = XCTestExpectation(description: "Waiting for task to start")
let task = Task {
DispatchQueue.main.async { startExpectation.fulfill() }
await vm.task()
}
wait(for: [startExpectation], timeout: 1)
let expectedCount = 1
let waitForCountExpectation = XCTestExpectation(description: "Waiting for count")
Task {
for await value in vm.$count.values where value >= expectedCount {
break
}
waitForCountExpectation.fulfill()
}
Task {
for _ in 0..<expectedCount {
await NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil)
}
}
wait(for: [waitForCountExpectation], timeout: 5)
XCTAssertEqual(vm.count, expectedCount)
task.cancel()
}
}
The main points:
- The test is NOT marked with @MainActor or as async. For some reason, it work fine with one or the other, but once you have both, waiting for the expectations doesn't work. Neither seemed necessary for the test though, so I removed them.
- An expectation is used to wait for the initial task to run. The fulfillment of the expectation is dispatched to the main queue just to allow the view model to actual start its task first.
- The underlying publisher, vm.$counts, is used to observe when the publisher actually updates its values in response to the notifications. Another expectation is used to wait for the proper number of notifications to be received.