11

We can test thrown errors with XCTAssertThrowsError. Async things can be tested with expectation. I have some method which dispatch work to a background thread and can at some point throw an error.

Is it possible to expect an error be thrown somewhere in the future? I need to combine expectation and XCTAssertThrowsError I think, but I do not know how.

Reproduction project: https://github.com/Jasperav/ThrowingAsyncError. Just clone the project and run the tests, one of them will fail. I made a class which will crash after a few seconds after it has been allocated. I want to make sure it keeps crashing after a few seconds, so I want a test case for it.

J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • If the functions your testing throw. Shouldn't you just be doing a do / catch. –  Amerino Oct 30 '19 at 21:28
  • @Amerino Doesn't work when the methods are async. – J. Doe Oct 30 '19 at 21:38
  • 1
    @J.Doe would it be possible to provide some sample code? That may make it a little bit easier to understand how exactly this background work is used. For instance, is this background work called with a completion handler, etc. – Anthony Dito Nov 06 '19 at 01:51
  • Would you be able to just make your unit test throw as well? Then if the async call throws within the unit test, assuming you're not catching the error it would fail the test. – jlowe Nov 06 '19 at 15:22
  • @AnthonyDito Ok, I made one, see my edit – J. Doe Nov 06 '19 at 21:33
  • I am not sure, but maybe [this post] (https://stackoverflow.com/a/34301948/1987726) solves your problem? – Reinhard Männer Nov 07 '19 at 13:55
  • I deleted my previous answer which didn't answer your question. I did dig into it a bit more, and 1) I'm not sure you can ever catch a fatalError, 2) even if you replace that with a thrown exception, I'm not sure you can catch a throw on an async thread unless you have some way to insert a closure into that thread. (And even then, it's not clear to me exactly how you'd do it.) – ajgryc Nov 08 '19 at 04:26

5 Answers5

4

Using the other answers here, I wrote a helper function that works as a drop-in replacement for XCTAssertThrowsError when working with async functions.

import XCTest

/// Asserts that an asynchronous expression throws an error.
/// (Intended to function as a drop-in asynchronous version of `XCTAssertThrowsError`.)
///
/// Example usage:
///
///     await assertThrowsAsyncError(
///         try await sut.function()
///     ) { error in
///         XCTAssertEqual(error as? MyError, MyError.specificError)
///     }
///
/// - Parameters:
///   - expression: An asynchronous expression that can throw an error.
///   - message: An optional description of a failure.
///   - file: The file where the failure occurs.
///     The default is the filename of the test case where you call this function.
///   - line: The line number where the failure occurs.
///     The default is the line number where you call this function.
///   - errorHandler: An optional handler for errors that expression throws.
func assertThrowsAsyncError<T>(
    _ expression: @autoclosure () async throws -> T,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line,
    _ errorHandler: (_ error: Error) -> Void = { _ in }
) async {
    do {
        _ = try await expression()
        // expected error to be thrown, but it was not
        let customMessage = message()
        if customMessage.isEmpty {
            XCTFail("Asynchronous call did not throw an error.", file: file, line: line)
        } else {
            XCTFail(customMessage, file: file, line: line)
        }
    } catch {
        errorHandler(error)
    }
}

I’ve posted this as a file that can be added to the unit test target of your projects: https://gitlab.com/-/snippets/2567566

Grant Neufeld
  • 549
  • 1
  • 9
  • 18
3

I took the sample code from David B.'s answer and changed it, because expectations are not needed when the unit test method is annotated with async.

func testFailingAsyncCode() async { // async is important here
    let dataFetcher = DataFetcher()
    
    var didFailWithError: Error?
    do {
        // This call is expected to fail
        _ = try await dataFetcher.fetchData(withRequest: request, validStatusCodes: [200])
    } catch {
        didFailWithError = error
        // Here you could do more assertions with the non-nil error object
    }

    XCTAssertNotNil(didFailWithError)
}
heyfrank
  • 5,291
  • 3
  • 32
  • 46
2

You could fulfill an expectation in the catch block of a call that is expected to fail.

func testFailingAsyncCode() async throws {
    let expectation = expectation(description: "expect call to throw error")
    let dataFetcher = DataFetcher()
    
    do {
        // This call is expected to fail
        let data = try await dataFetcher.fetchData(withRequest: request, validStatusCodes: [200])
    } catch {
        // Expectation is fulfilled when call fails
        expectation.fulfill()
    }
    
    wait(for: [expectation], timeout: 3)
}
David B.
  • 413
  • 4
  • 6
2

Approach:

  • Use async
  • Use XCTFail
func testFailingAsyncCode() async {
    let dataFetcher = DataFetcher()
    
    do {
        _ = try await dataFetcher.fetchData(withRequest: request, validStatusCodes: [200])
        XCTFail("Error needs to be thrown")
    } catch {
        //Do nothing, if error is thrown then it matches expected result
    }
}
user1046037
  • 16,755
  • 12
  • 92
  • 138
-1

I took a look at the reproduction project to see what you were trying to accomplish here...

To my understanding:

XCTAssertThrowsError are assertions that takes in a block that can throw. They just happen to assert that an error is thrown in a synchronous block when it's done running.

XCTestExpectation are classes that keep track of whether or not requested conditions are met. They are for keeping track of asynchronous code behavior objects/references need to be kept and checked later.

What you seem to be trying to do is make something like XCTestExpectation work the same way XCTAssertThrowsError does, as in make an synchronous assertion that an asynchronous block will throw. It won't work quite that way because of how the code runs and returns.

The asynchronous code you refer to does not throw (timer initializer). As far as I know, there aren't any asynchronous blocks that can throw. Perhaps the question you should be asking is how can we make a synchronous operation choose to run synchronously sometimes, but also asynchronously when it feels like...

Alternatively for some additional complexity in every class you would like to test I've made a solution with what is almost bare minimum to make this easily testable and portable...

https://github.com/Jasperav/ThrowingAsyncError/pull/1/files

May I ask why you would ever want to do something like this?