diff --git a/Sources/XCTest/Public/XCTAssert.swift b/Sources/XCTest/Public/XCTAssert.swift index d3249740..87375319 100644 --- a/Sources/XCTest/Public/XCTAssert.swift +++ b/Sources/XCTest/Public/XCTAssert.swift @@ -28,7 +28,9 @@ private enum _XCTAssertion { case `false` case fail case throwsError + case throwsErrorAsync case noThrow + case noThrowAsync var name: String? { switch(self) { @@ -48,7 +50,9 @@ private enum _XCTAssertion { case .`true`: return "XCTAssertTrue" case .`false`: return "XCTAssertFalse" case .throwsError: return "XCTAssertThrowsError" + case .throwsErrorAsync: return "XCTAssertThrowsErrorAsync" case .noThrow: return "XCTAssertNoThrow" + case .noThrowAsync: return "XCTAssertNoThrowAsync" case .fail: return nil } } @@ -105,6 +109,28 @@ private func _XCTEvaluateAssertion(_ assertion: _XCTAssertion, message: @autoclo } } +private func _XCTEvaluateAssertionAsync(_ assertion: _XCTAssertion, message: @autoclosure () async -> String, file: StaticString, line: UInt, expression: () async throws -> _XCTAssertionResult) async { + let result: _XCTAssertionResult + do { + result = try await expression() + } catch { + result = .unexpectedFailure(error) + } + + switch result { + case .success: + return + default: + if let currentTestCase = XCTCurrentTestCase { + currentTestCase.recordFailure( + withDescription: "\(result.failureDescription(assertion)) - \(await message())", + inFile: String(describing: file), + atLine: Int(line), + expected: result.isExpected) + } + } +} + /// This function emits a test failure if the general `Boolean` expression passed /// to it evaluates to `false`. /// @@ -432,6 +458,24 @@ public func XCTAssertThrowsError(_ expression: @autoclosure () throws -> T, _ } } +public func XCTAssertThrowsErrorAsync(_ expression: @autoclosure () async throws -> T, _ message: @autoclosure () async -> String = "", file: StaticString = #file, line: UInt = #line, _ errorHandler: (_ error: Swift.Error) async -> Void = { _ in }) async { + let rethrowsOverload: (() async throws -> T, () async -> String, StaticString, UInt, (Swift.Error) async throws -> Void) async throws -> Void = XCTAssertThrowsErrorAsync + + try? await rethrowsOverload(expression, message, file, line, errorHandler) +} + +public func XCTAssertThrowsErrorAsync(_ expression: @autoclosure () async throws -> T, _ message: @autoclosure () async -> String = "", file: StaticString = #file, line: UInt = #line, _ errorHandler: (_ error: Swift.Error) async throws -> Void = { _ in }) async rethrows { + await _XCTEvaluateAssertionAsync(.throwsErrorAsync, message: await message(), file: file, line: line) { + do { + _ = try await expression() + return .expectedFailure("did not throw error") + } catch { + try await errorHandler(error) + return .success + } + } +} + public func XCTAssertNoThrow(_ expression: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) { _XCTEvaluateAssertion(.noThrow, message: message(), file: file, line: line) { do { @@ -442,3 +486,14 @@ public func XCTAssertNoThrow(_ expression: @autoclosure () throws -> T, _ mes } } } + +public func XCTAssertNoThrowAsync(_ expression: @autoclosure () async throws -> T, _ message: @autoclosure () async -> String = "", file: StaticString = #file, line: UInt = #line) async { + await _XCTEvaluateAssertionAsync(.noThrowAsync, message: await message(), file: file, line: line) { + do { + _ = try await expression() + return .success + } catch let error { + return .expectedFailure("threw error \"\(error)\"") + } + } +} diff --git a/Tests/Functional/ErrorHandling/main.swift b/Tests/Functional/ErrorHandling/main.swift index 8d0511b1..6cec6606 100644 --- a/Tests/Functional/ErrorHandling/main.swift +++ b/Tests/Functional/ErrorHandling/main.swift @@ -21,7 +21,14 @@ class ErrorHandling: XCTestCase { ("test_shouldButDoesNotThrowErrorInAssertion", test_shouldButDoesNotThrowErrorInAssertion), ("test_shouldThrowErrorInAssertion", test_shouldThrowErrorInAssertion), ("test_throwsErrorInAssertionButFailsWhenCheckingError", test_throwsErrorInAssertionButFailsWhenCheckingError), - + + // Tests for XCTAssertThrowsErrorAsync + ("test_shouldRethrowErrorFromHandlerAsync", asyncTest(test_shouldRethrowErrorFromHandlerAsync)), + ("test_shouldNotRethrowWhenHandlerDoesNotThrowAsync", asyncTest(test_shouldNotRethrowWhenHandlerDoesNotThrowAsync)), + ("test_shouldButDoesNotThrowErrorInAssertionAsync", asyncTest(test_shouldButDoesNotThrowErrorInAssertionAsync)), + ("test_shouldThrowErrorInAssertionAsync", asyncTest(test_shouldThrowErrorInAssertionAsync)), + ("test_throwsErrorInAssertionButFailsWhenCheckingErrorAsync", asyncTest(test_throwsErrorInAssertionButFailsWhenCheckingErrorAsync)), + // Tests for "testFoo() throws" ("test_canAndDoesThrowErrorFromTestMethod", test_canAndDoesThrowErrorFromTestMethod), ("test_canButDoesNotThrowErrorFromTestMethod", test_canButDoesNotThrowErrorFromTestMethod), @@ -33,6 +40,10 @@ class ErrorHandling: XCTestCase { ("test_shouldNotThrowErrorDefiningSuccess", test_shouldNotThrowErrorDefiningSuccess), ("test_shouldThrowErrorDefiningFailure", test_shouldThrowErrorDefiningFailure), + // Tests for XCTAssertAsyncNoThrow + ("test_shouldNotThrowErrorDefiningSuccessAsync", asyncTest(test_shouldNotThrowErrorDefiningSuccessAsync)), + ("test_shouldThrowErrorDefiningFailureAsync", asyncTest(test_shouldThrowErrorDefiningFailureAsync)), + // Tests for XCTUnwrap ("test_shouldNotThrowErrorOnUnwrapSuccess", test_shouldNotThrowErrorOnUnwrapSuccess), ("test_shouldThrowErrorOnUnwrapFailure", test_shouldThrowErrorOnUnwrapFailure), @@ -51,7 +62,10 @@ class ErrorHandling: XCTestCase { func functionThatDoesNotThrowError() throws { } - + + func functionThatDoesNotThrowErrorAsync() async throws { + } + enum SomeError: Swift.Error { case anError(String) case shouldNotBeReached @@ -61,6 +75,10 @@ class ErrorHandling: XCTestCase { throw SomeError.anError("an error message") } + func functionThatDoesThrowErrorAsync() async throws { + throw SomeError.anError("an error message") + } + // CHECK: Test Case 'ErrorHandling.test_shouldRethrowErrorFromHandler' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ // CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+3]]: error: ErrorHandling.test_shouldRethrowErrorFromHandler : XCTAssertThrowsError threw error "anError\("an error message"\)" - // CHECK: Test Case 'ErrorHandling.test_shouldRethrowErrorFromHandler' failed \(\d+\.\d+ seconds\) @@ -118,6 +136,63 @@ class ErrorHandling: XCTestCase { } } +// CHECK: Test Case 'ErrorHandling.test_shouldRethrowErrorFromHandlerAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+3]]: error: ErrorHandling.test_shouldRethrowErrorFromHandlerAsync : XCTAssertThrowsErrorAsync threw error "anError\("an error message"\)" - +// CHECK: Test Case 'ErrorHandling.test_shouldRethrowErrorFromHandlerAsync' failed \(\d+\.\d+ seconds\) + func test_shouldRethrowErrorFromHandlerAsync() async throws { + try await XCTAssertThrowsErrorAsync(await functionThatDoesThrowErrorAsync()) {_ in try await functionThatDoesThrowErrorAsync() } + } + +// CHECK: Test Case 'ErrorHandling.test_shouldNotRethrowWhenHandlerDoesNotThrowAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ErrorHandling.test_shouldNotRethrowWhenHandlerDoesNotThrowAsync' passed \(\d+\.\d+ seconds\) + func test_shouldNotRethrowWhenHandlerDoesNotThrowAsync() async throws { + try await XCTAssertThrowsErrorAsync(await functionThatDoesThrowErrorAsync()) {_ in try await functionThatDoesNotThrowErrorAsync() } + } + +// CHECK: Test Case 'ErrorHandling.test_shouldButDoesNotThrowErrorInAssertionAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+3]]: error: ErrorHandling.test_shouldButDoesNotThrowErrorInAssertionAsync : XCTAssertThrowsErrorAsync failed: did not throw error - +// CHECK: Test Case 'ErrorHandling.test_shouldButDoesNotThrowErrorInAssertionAsync' failed \(\d+\.\d+ seconds\) + func test_shouldButDoesNotThrowErrorInAssertionAsync() async throws { + await XCTAssertThrowsErrorAsync(try await functionThatDoesNotThrowErrorAsync()) + } + +// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorInAssertionAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorInAssertionAsync' passed \(\d+\.\d+ seconds\) + func test_shouldThrowErrorInAssertionAsync() async throws { + await XCTAssertThrowsErrorAsync(try await functionThatDoesThrowErrorAsync()) { error in + guard let thrownError = error as? SomeError else { + XCTFail("Threw the wrong type of error") + return + } + + switch thrownError { + case .anError(let message): + XCTAssertEqual(message, "an error message") + default: + XCTFail("Unexpected error: \(thrownError)") + } + } + } + +// CHECK: Test Case 'ErrorHandling.test_throwsErrorInAssertionButFailsWhenCheckingErrorAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+11]]: error: ErrorHandling.test_throwsErrorInAssertionButFailsWhenCheckingErrorAsync : XCTAssertEqual failed: \("an error message"\) is not equal to \(""\) - +// CHECK: Test Case 'ErrorHandling.test_throwsErrorInAssertionButFailsWhenCheckingErrorAsync' failed \(\d+\.\d+ seconds\) + func test_throwsErrorInAssertionButFailsWhenCheckingErrorAsync() async throws { + await XCTAssertThrowsErrorAsync(try await functionThatDoesThrowErrorAsync()) { error in + guard let thrownError = error as? SomeError else { + XCTFail("Threw the wrong type of error") + return + } + + switch thrownError { + case .anError(let message): + XCTAssertEqual(message, "") + default: + XCTFail("Unexpected error: \(thrownError)") + } + } + } + // CHECK: Test Case 'ErrorHandling.test_canAndDoesThrowErrorFromTestMethod' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ // CHECK: \:0: error: ErrorHandling.test_canAndDoesThrowErrorFromTestMethod : threw error "anError\("an error message"\)" // CHECK: Test Case 'ErrorHandling.test_canAndDoesThrowErrorFromTestMethod' failed \(\d+\.\d+ seconds\) @@ -156,6 +231,19 @@ class ErrorHandling: XCTestCase { XCTAssertNoThrow(try functionThatDoesThrowError()) } +// CHECK: Test Case 'ErrorHandling.test_shouldNotThrowErrorDefiningSuccessAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: Test Case 'ErrorHandling.test_shouldNotThrowErrorDefiningSuccessAsync' passed \(\d+\.\d+ seconds\) + func test_shouldNotThrowErrorDefiningSuccessAsync() async { + await XCTAssertNoThrowAsync(try await functionThatDoesNotThrowErrorAsync()) + } + +// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorDefiningFailureAsync' started at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ +// CHECK: .*[/\\]ErrorHandling[/\\]main.swift:[[@LINE+3]]: error: ErrorHandling.test_shouldThrowErrorDefiningFailureAsync : XCTAssertNoThrowAsync failed: threw error "anError\("an error message"\)" - +// CHECK: Test Case 'ErrorHandling.test_shouldThrowErrorDefiningFailureAsync' failed \(\d+\.\d+ seconds\) + func test_shouldThrowErrorDefiningFailureAsync() async { + await XCTAssertNoThrowAsync(try await functionThatDoesThrowErrorAsync()) + } + func functionShouldReturnOptionalButThrows() throws -> String? { throw SomeError.anError("an error message") } @@ -293,11 +381,11 @@ class ErrorHandling: XCTestCase { } // CHECK: Test Suite 'ErrorHandling' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed \d+ tests, with \d+ failures \(6 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed \d+ tests, with \d+ failures \(7 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds XCTMain([testCase(ErrorHandling.allTests)]) // CHECK: Test Suite '.*\.xctest' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed \d+ tests, with \d+ failures \(6 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed \d+ tests, with \d+ failures \(7 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds // CHECK: Test Suite 'All tests' failed at \d+-\d+-\d+ \d+:\d+:\d+\.\d+ -// CHECK: \t Executed \d+ tests, with \d+ failures \(6 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds +// CHECK: \t Executed \d+ tests, with \d+ failures \(7 unexpected\) in \d+\.\d+ \(\d+\.\d+\) seconds