amplify-swift/AmplifyAsyncTesting/Sources/AsyncTesting/XCTestCase+AsyncTesting.swift

181 lines
7.9 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Foundation
import XCTest
extension XCTestCase {
public static let defaultTimeoutForAsyncExpectations = TimeInterval(60)
public static let defaultNetworkTimeoutForAsyncExpectations = TimeInterval(600)
/// Creates a new async expectation with an associated description.
///
/// Use this method to create ``AsyncExpectation`` instances that can be
/// fulfilled when asynchronous tasks in your tests complete.
///
/// To fulfill an expectation that was created with `asyncExpectation(description:)`,
/// call the expectation's `fulfill()` method when the asynchronous task in your
/// test has completed.
///
/// - Parameters:
/// - description: A string to display in the test log for this expectation, to help diagnose failures.
/// - isInverted: Indicates that the expectation is not intended to happen.
/// - expectedFulfillmentCount: The number of times fulfill() must be called before the expectation is completely fulfilled. (default = 1)
public func asyncExpectation(description: String,
isInverted: Bool = false,
expectedFulfillmentCount: Int = 1) -> AsyncExpectation {
AsyncExpectation(description: description,
isInverted: isInverted,
expectedFulfillmentCount: expectedFulfillmentCount)
}
/// Waits for the test to fulfill a set of expectations within a specified time.
/// - Parameters:
/// - expectations: An array of async expectations that must be fulfilled.
/// - timeout: The number of seconds within which all expectations must be fulfilled.
@MainActor
public func waitForExpectations(_ expectations: [AsyncExpectation],
timeout: Double = 1.0,
file: StaticString = #filePath,
line: UInt = #line) async {
await AsyncTesting.waitForExpectations(expectations,
timeout: timeout,
file: file,
line: line)
}
/// Run a task with a timeout using an `AsyncExpectation`.
/// - Parameters:
/// - timeout: timeout
/// - operation: operation to run
/// - Returns: result of closure
@discardableResult
public func testTask<Success>(timeout: Double = defaultTimeoutForAsyncExpectations,
file: StaticString = #filePath,
line: UInt = #line,
@_implicitSelfCapture operation: @escaping @Sendable () async throws -> Success) async throws -> Success {
let done = asyncExpectation(description: "done")
let task = Task { () -> Success in
let result = try await operation()
await done.fulfill()
return result
}
await waitForExpectations([done], timeout: timeout, file: file, line: line)
return try await task.value
}
/// Waits for the execution of a given async code, using a given async expectation,
/// and returns its result or `nil` if it threw an error.
///
/// This method will automatically call the expectation's `fulfill()` method when the async
/// code finishes, either successfully or with error.
///
/// - Parameters:
/// - expectation: The async expectation that will be fulfilled once the code in `action` completes.
/// - timeout: How long to wait for `action` to complete. Defaults to 10 seconds.
/// - action: Closure containing async code.
///
/// - Returns:The result of successfuly running `action`, or `nil` if it threw an error.
@discardableResult
func wait<T>(with expectation: AsyncExpectation,
timeout: TimeInterval = defaultNetworkTimeoutForAsyncExpectations,
action: @escaping () async throws -> T) async -> T? {
let task = Task { () -> T? in
defer {
Task {
await expectation.fulfill()
}
}
do {
return try await action()
} catch {
if !(error is CancellationError) {
XCTFail("Failed with \(error)")
}
return nil
}
}
await waitForExpectations([expectation], timeout: timeout)
task.cancel()
return await task.value
}
/// Waits for the execution of a given async code and returns its result or `nil` if it threw an error.
///
/// - Parameters:
/// - name: The name that will be used to create the async expectation that will be fulfilled once the code in `action` completes.
/// - timeout: How long to wait for `action` to complete. Defaults to 10 seconds.
/// - action: Closure containing async code.
///
/// - Returns:The result of successfuly running `action`, or `nil` if it threw an error.
@discardableResult
func wait<T>(name: String,
timeout: TimeInterval = defaultNetworkTimeoutForAsyncExpectations,
action: @escaping () async throws -> T) async -> T? {
let expectation = asyncExpectation(description: name)
return await wait(with: expectation, timeout: timeout, action: action)
}
/// Waits for an error during the execution of a given async code, using a given async expectation,
/// and returns said error or `nil` if it run successfully.
///
/// This method will automatically call the expectation's `fulfill()` method when the async
/// code finishes, either successfully or with error.
///
/// - Parameters:
/// - expectation: The async expectation that will be fulfilled once the code in `action` completes.
/// - timeout: How long to wait for `action` to complete. Defaults to 10 seconds.
/// - action: Closure containing async code.
///
/// - Returns:The error thrown during the execution of `action`, or `nil` if it run successfully.
@discardableResult
func waitError<T>(with expectation: AsyncExpectation,
timeout: TimeInterval = defaultNetworkTimeoutForAsyncExpectations,
action: @escaping () async throws -> T) async -> Error? {
let task = Task { () -> Error? in
defer {
Task { await expectation.fulfill() }
}
do {
let result = try await action()
XCTFail("Should not have completed, got \(result)")
return nil
} catch {
if error is CancellationError {
return nil
}
return error
}
}
await waitForExpectations([expectation], timeout: timeout)
task.cancel()
return await task.value
}
/// Waits for an error during the execution of a given async code and returns said error
/// or `nil` if it run successfully.
///
///
/// - Parameters:
/// - expectation: The async expectation that will be fulfilled once the code in `action` completes.
/// - timeout: How long to wait for `action` to complete. Defaults to 10 seconds.
/// - action: Closure containing async code.
///
/// - Returns:The error thrown during the execution of `action`, or `nil` if it run successfully.
@discardableResult
func waitError<T>(name: String,
timeout: TimeInterval = defaultNetworkTimeoutForAsyncExpectations,
action: @escaping () async throws -> T) async -> Error? {
let expectation = asyncExpectation(description: name)
return await waitError(with: expectation, timeout: timeout, action: action)
}
}