Testing async code in Swift - Time & Space Complexity
When testing async code, we want to know how the time to complete grows as the tasks increase.
We ask: How does waiting for asynchronous tasks affect overall execution time?
Analyze the time complexity of the following async code snippet.
func fetchData(ids: [Int]) async {
for id in ids {
let data = await fetchFromNetwork(id: id)
print(data)
}
}
func fetchFromNetwork(id: Int) async -> String {
// Simulates network delay
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "Data for \(id)"
}
This code fetches data for each id one after another, waiting for each network call to finish before starting the next.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The for-loop that calls
fetchFromNetworkfor each id. - How many times: Once for each element in the input array
ids.
Each network call waits about 1 second before returning. Since calls happen one after another, total time adds up.
| Input Size (n) | Approx. Operations (seconds) |
|---|---|
| 10 | ~10 seconds |
| 100 | ~100 seconds |
| 1000 | ~1000 seconds |
Pattern observation: Time grows linearly as the number of ids increases because each call waits fully before the next starts.
Time Complexity: O(n)
This means the total time grows directly in proportion to the number of async calls made one after another.
[X] Wrong: "Async calls always run at the same time, so total time stays constant no matter how many calls."
[OK] Correct: In this code, calls happen one after another because of the await inside the loop, so times add up instead of overlapping.
Understanding how async code runs step-by-step helps you explain performance clearly and shows you can reason about real-world delays and waiting times.
What if we changed the code to start all fetches at once and then wait for all to finish? How would the time complexity change?
Practice
Solution
Step 1: Understand async test function requirement
To test asynchronous code, the test function itself must be marked as asynchronous.Step 2: Identify the correct keyword
The keyword to mark a function as asynchronous in Swift isasync.Final Answer:
async -> Option CQuick Check:
async = async [OK]
- Using await instead of async on test function
- Forgetting to mark test function as async
- Using try instead of async
- Confusing defer with async
fetchData() inside a test?Solution
Step 1: Recall await usage in Swift
In Swift, to wait for an async function, you use theawaitkeyword before the function call.Step 2: Check correct syntax
The correct syntax islet result = await fetchData(). The keywordawaitcomes before the function call.Final Answer:
let result = await fetchData() -> Option AQuick Check:
await before function call = let result = await fetchData() [OK]
- Placing await after let or variable
- Using async instead of await before call
- Calling await as a method
- Omitting await keyword
func testFetch() async {
let data = try? await fetchData()
print(data == nil ? "No Data" : "Got Data")
}
func fetchData() async throws -> String {
return "Hello"
}Solution
Step 1: Analyze fetchData() behavior
The functionfetchData()is async and throws but always returns "Hello" without throwing an error.Step 2: Understand try? await usage
Thetry? await fetchData()will return the string "Hello" wrapped as optional, not nil, because no error is thrown.Step 3: Evaluate print statement
Sincedatais not nil, the print outputs "Got Data".Final Answer:
Got Data -> Option DQuick Check:
try? await returns value = Got Data [OK]
- Assuming try? always returns nil
- Confusing async throws with throwing error always
- Ignoring optional unwrapping
- Expecting compilation error due to async
func testDownload() async {
let result = fetchData()
XCTAssertEqual(result, "Success")
}
func fetchData() async -> String {
return "Success"
}Solution
Step 1: Check async function call
The functionfetchData()is async, so calling it requiresawaitto get the result.Step 2: Identify missing await
In the test,fetchData()is called withoutawait, causing a compile error.Final Answer:
Missing await before fetchData() call -> Option BQuick Check:
Async calls need await = Missing await before fetchData() call [OK]
- Calling async function without await
- Thinking test functions can't be async
- Misusing XCTAssertEqual with strings
- Assuming async functions must throw
loadUser(id:) that throws an error if the user is not found. Which test code correctly checks that an error is thrown for an invalid id?Solution
Step 1: Understand error throwing test pattern
To test that an async function throws, use a do-catch block and call the function with try await.Step 2: Verify correct error handling
func testLoadUserError() async throws { do { _ = try await loadUser(id: -1) XCTFail("Expected error not thrown") } catch { XCTAssertTrue(true) } } callsloadUser(id: -1)with try await inside do, fails test if no error, and passes if error caught.Step 3: Check other options
Approaches expecting a nil user incorrectly assume throwing functions return nil instead of throwing. func testLoadUserError() async throws { _ = try await loadUser(id: -1) XCTAssertTrue(false) } fails test unconditionally.Final Answer:
do { _ = try await loadUser(id: -1) XCTFail("Expected error not thrown") } catch { XCTAssertTrue(true) } -> Option AQuick Check:
Use do-catch with try await to test errors [OK]
- Expecting nil instead of catching error
- Omitting try or await keywords
- Failing test unconditionally
- Not marking test async throws
Ready to prove it?
Pass the test and this pattern moves unknown
