Modern Concurrency: Beyond the Basics

Oct 20 2022 · Swift 5.5, iOS 15, Xcode 13.4

Part 1: AsyncStream & Continuations

09. Unit Testing Tools

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 08. Wrapping Callback With Continuation Next episode: 10. Conclusion

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

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

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🟥) {