I don't think that testing techniques that rely on special scheduling modes would be ideal; in addition to being fundamentally brittle, that take the environment you're testing further away from the actual production environment the code will run in, making the test less effective. The sorts of tools swift-async-algorithms has may have their place but shouldn't be necessary for everyday testing. If we can fit synchronization points into the code that the test can connect with, then the test should be robust regardless of the environment it runs in. I don't think you need to radically rewrite your example to do that. For instance, you could use swift-async-algorithms' AsyncChannel to send sentinels at significant points in the ViewModel task's execution:
class ViewModel: ObservableObject {
@Published var count = 0
enum Progress {
case didSubscribeToScreenshots, didRespondToScreenshot
}
// set to non-nil when testing
var progress: AsyncChannel<Progress>?
@MainActor
func task() async {
let screenshots = NotificationCenter.default.notifications(
named: UIApplication.userDidTakeScreenshotNotification
)
await progress?.send(.didSubscribeToScreenshots)
for await _ in screenshots {
self.count += 1
await progress?.send(.didRespondToScreenshot)
}
}
}
In production, the channel property can be left as nil, and execution will proceed normally. But in testing, instead of hoping we get lucky with scheduling, the test can install a channel into the view model, and await key points of the ViewModel task's execution, while also asserting that the ViewModel task's progress corresponds to the test's expectations:
@MainActor
func testBasics() async throws {
let vm = ViewModel()
// Install a progress channel into the ViewModel we can monitor
let vmProgress = AsyncChannel<ViewModel.Progress>()
vm.progress = vmProgress
// We get `task.cancel(); await task.value` for free with async let
async let _ = vm.task()
XCTAssertEqual(vm.count, 0)
// Give the task an opportunity to start executing its work.
let firstProgress = await vmProgress.next()
XCTAssertEqual(firstProgress, .didSubscribeToScreenshots)
// Simulate a screen shot being taken.
NotificationCenter.default.post(
name: UIApplication.userDidTakeScreenshotNotification, object: nil
)
// Give the task an opportunity to update the view model.
let nextProgress = await vmProgress.next()
XCTAssertEqual(nextProgress, .didRespondToScreenshot)
XCTAssertEqual(vm.count, 1)
}
Combine is perhaps easier to test because you get these probe points "for free" between each layer of transformation, whereas async-await makes it easier to write lots of straight-line code that mixes concerns, for better or worse. It's another front in the functional-vs-imperative war in that regard.