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’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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