Modern Concurrency: Beyond the Basics

Oct 20 2022 Swift 5.5, iOS 15, Xcode 13.4

Part 1: AsyncStream & Continuations

9. Unit Testing Tools

Episode complete

Play next episode

Next
Save for later
About this episode
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 8. Wrapping Callback With Continuation Next episode: 10. Conclusion

This video Unit Testing Tools was last updated on Oct 20 2022

Heads up... You've reached locked video content where the transcript will be shown as obfuscated text.

You can unlock the rest of this video course, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.

In episode 6, when you wrote unit tests, there were two issues: await doesn’t time out, and the tests take more than 5 seconds to run, because of the 1-second waits built into the countdown.

Adding TimeoutTask for safer testing

You can’t let your tests hang indefinitely, so you’ll create a new type called TimeoutTask. It’s like Task except it throws an error if the asynchronous code doesn’t complete in time.

class TimeoutTask<Success> {

}
extension TimeoutTask {
  struct TimeoutError: LocalizedError {

  }
}
extension TimeoutTask {
  struct TimeoutError: LocalizedError {
    🟩
    var errorDescription: String? {
      return "The operation timed out."
    }
    🟥
  }
}
let nanoseconds: UInt64
let operation: @Sendable () async throws -> Success
init(
  seconds: TimeInterval, 
  
) {

}
init(
  seconds: TimeInterval, 
  🟩
  operation: @escaping @Sendable () async throws -> Success
  🟥
) {  
 
}
init(
  seconds: TimeInterval, 
  operation: @escaping @Sendable () async throws -> Success
) {  
  🟩
  self.nanoseconds = UInt64(seconds * 1_000_000_000)  // 1 billion nanoseconds
  self.operation = operation
  🟥
}
private var continuation: CheckedContinuation<Success, Error>?
var value: Success {
  get async throws {

  }
}
private var continuation: CheckedContinuation<Success, Error>?

var value: Success {
  get async throws {
    🟩
    try await withCheckedThrowingContinuation { continuation in
      self.continuation = continuation
    }
    🟥
  }
}
var value: Success {
  get async throws {
    try await withCheckedThrowingContinuation { continuation in
      self.continuation = continuation
      🟩
      Task {
        try await Task.sleep(nanoseconds: nanoseconds)

      }    
      🟥
    }
  }
}
var value: Success {
  get async throws {
    try await withCheckedThrowingContinuation { continuation in
      self.continuation = continuation
      Task {
        try await Task.sleep(nanoseconds: nanoseconds)
        🟩
        self.continuation?.resume(throwing: TimeoutError())
        self.continuation = nil  // then destroy the continuation
        🟥
      }    
    }
  }
}
Task {
  try await Task.sleep(nanoseconds: nanoseconds)
  self.continuation?.resume(throwing: TimeoutError())
  self.continuation = nil
}
🟩
Task {
  let result = try await operation()  // execute the operaton passed into the initializer
  self.continuation?.resume(returning: result)  // return the result
  self.continuation = nil  // and destroy the continuation
}
🟥

Canceling your task

For completeness, add one more method to TimeoutTask:

func cancel() {
  continuation?.resume(throwing: CancellationError())
  continuation = nil
}

Using TimeoutTask

So how to use your new TimeoutTask in your unit test? In BlabberTests, look at testModelCountdown().

}
.value

Speeding up asynchronous tests

You’ll use a mock Task.sleep to inject a time dependency so you can set time to go faster in your tests.

var sleep: (UInt64) async throws -> Void = Task.sleep(nanoseconds:)
let sleep = self.sleep
try await sleep(1_000_000_000)

Updating the tests

In BlabberTests, add this line to the end of your model definition (before return model):

model.sleep = {   
  try await Task.sleep(nanoseconds: $0 / 1_000_000_000) 
}

Testing TimeoutTask

Now, to see if TimeoutTask really times out:

async let messages = TimeoutTask(seconds: 🟩1🟥) {
  await TestURLProtocol.requests
    .prefix(🟩5🟥)
Test Suite 'All tests' started at 2021-12-29 11:58:02.211
Test Suite 'BlabberTests.xctest' started at 2021-12-29 11:58:02.212
Test Suite 'BlabberTests' started at 2021-12-29 11:58:02.212
Test Case '-[BlabberTests.BlabberTests testModelCountdown]' started.
<unknown>:0: error: -[BlabberTests.BlabberTests testModelCountdown] : failed: caught error: "The operation timed out."
Test Case '-[BlabberTests.BlabberTests testModelCountdown]' failed (1.072 seconds).
Test Case '-[BlabberTests.BlabberTests testModelSay]' started.
Test Case '-[BlabberTests.BlabberTests testModelSay]' passed (0.003 seconds).
Test Suite 'BlabberTests' failed at 2021-12-29 11:58:03.287.
	 Executed 2 tests, with 1 failure (1 unexpected) in 1.075 (1.076) seconds
Test Suite 'BlabberTests.xctest' failed at 2021-12-29 11:58:03.288.
	 Executed 2 tests, with 1 failure (1 unexpected) in 1.075 (1.076) seconds
Test Suite 'All tests' failed at 2021-12-29 11:58:03.288.
	 Executed 2 tests, with 1 failure (1 unexpected) in 1.075 (1.077) seconds
Test Suite 'Selected tests' started at 2021-12-29 12:04:16.651
Test Suite 'BlabberTests.xctest' started at 2021-12-29 12:04:16.652
Test Suite 'BlabberTests' started at 2021-12-29 12:04:16.652
Test Case '-[BlabberTests.BlabberTests testModelCountdown]' started.
Test Case '-[BlabberTests.BlabberTests testModelCountdown]' passed (0.012 seconds).
Test Suite 'BlabberTests' passed at 2021-12-29 12:04:16.664.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.012 (0.013) seconds
Test Suite 'BlabberTests.xctest' passed at 2021-12-29 12:04:16.665.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.012 (0.013) seconds
Test Suite 'Selected tests' passed at 2021-12-29 12:04:16.665.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.012 (0.014) seconds
async let messages = TimeoutTask(seconds: 🟩10🟥) {