Skip to content

Instantly share code, notes, and snippets.

@robertmryan
Created March 14, 2025 17:58
Show Gist options
  • Save robertmryan/654fff6d603bd4c358f3781d2f31b334 to your computer and use it in GitHub Desktop.
Save robertmryan/654fff6d603bd4c358f3781d2f31b334 to your computer and use it in GitHub Desktop.
class ImageManager {
func downloadInOrderIndependentDictionary() async throws -> [String: Image] {
try await withThrowingTaskGroup(of: (String, Image?).self) { group in
let items = await fetchList()
for item in items {
group.addTask {
do {
return try await (item.imageName, self.fetch(imageName: item.imageName))
} catch is CancellationError {
throw CancellationError()
} catch {
return (item.imageName, nil)
}
}
}
return try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
}
}
func fetchList() async -> [Item] {…}
func fetch(imageName: String) async throws -> Image {
try await Task.sleep(for: .seconds(1))
}
}
struct MyAppTests {
@Test func testDownloadTimeout() async throws {
let imageManager = ImageManager()
do {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
_ = try await imageManager.downloadInOrderIndependentDictionary()
#expect(Bool(false), "The download should have been canceled before finishing")
}
group.addTask {
try await Task.sleep(for: .seconds(0.5))
}
try await group.next()
group.cancelAll()
try await group.waitForAll()
}
#expect(Bool(false), "The above should have thrown an error and we should not get here")
} catch {
#expect(error is CancellationError)
}
}
}
@robertmryan
Copy link
Author

You asked:

Is it possible to add a unit test case to validate image download has been cancelled. For example there were 5 images requested to be downloaded, just after 1st image update is received cancel any further downloads? It's useful in case app. is being backgrounded we don't want any further downloads to happen.

Yes. You just need to make sure that your download throws CancellationError if canceled. And then your test can explicitly look for CancelationError.

The above example is using the contemporary Swift Testing framework, but the idea would be the same in the legacy XCTest framework, too (but, obviously #expect(Bool(false), …) would be replaced with XCTFail, #expect(error is CancellationError) would be replaced with the appropriate XCTAssert test, etc.

A few caveats:

  1. I would make sure the network request be stubbed with something that takes a pre-determined amount of time and does not actually perform a network request. Network tests do not belong in unit tests. (Integration tests, perhaps, but not unit tests.)
  2. If downloading assets that you are caching for future needs, I might consider patterns other than canceling the downloads. E.g., Extending your app’s background execution time if you need less than 30 seconds or Downloading files in the background if downloading lots of assets or extremely large assets.

@robertmryan
Copy link
Author

FWIW, the details of the above implementation are of less import than the general idea of just making sure your asynchronous routines throw CancellationError if canceled. Fortunately, most Apple cancelable async API do this already, so you just need to make sure you let those percolate up the call chain. Once you embrace the existing cancelable API, writing tests to detect cancelation are quite simple.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment