This video Unit Testing Tools was last updated on Oct 20 2022
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🟥) {