8

Googling has led me to general Swift error handling links, but I have a more specific question. I think the answer here is "no, you're out of luck" but I want to double check to see if I'm missing something. This question from a few years ago seems similar and has some answers with gross looking workarounds... I'm looking to see if the latest version of Swift makes something more elegant possible.

Situation: I have a function which is NOT marked with throws, and uses try!.

Goal: I want to create a unit test which verifies that, yep, giving this function the wrong thing will in fact fail and throw a (runtime/fatal) error.

Problems:

  1. When I wrap this function in a do-catch, the compiler warns me that the catch block is unreachable.
  2. When I run the test and pass in the bad arguments, the do-catch does NOT catch the error.
  3. XCTAssertThrows also does not catch the error.

This function is built to have an identical signature to another, silently failing function, which I swap it out for this one on simulators so that I can loudly fail during testing (either automated or manual). So I can't just change this to a throwing function, because then the other function will have to be marked as throwing and I want it to fail silently.

So, is there a way to throw an unhandled error that I can catch in a unit test?

Alternatively, can I make this function blow up in a testable way without changing the signature?

Elliot Schrock
  • 1,406
  • 2
  • 9
  • 20

5 Answers5

2

There is no way to catch non-throwing errors in swift and you mean that by using ! after try.

But you can refactor your code in a way you can have more control from outside of the function like this:

  1. Factor out the throwing function, so you can test it in the right way:
func throwingFunc() throws {
    let json = "catch me if you can".data(using: .utf8)!
    try JSONDecoder().decode(Int.self, from: json)
}
  1. Write a non-throwing wrapper with a custom error handler:
func nonThrowingFunc( catchHandler:((Error)->Void)? = nil ) {
    guard let handler = catchHandler else { return try! throwingFunc() }
    do {
        try throwingFunc()
    } catch {
        handler(error)
    }
}

So the handler will be called only if you are handling it:

// Test the function and faild the test if needed
nonThrowingFunc { error in
    XCTFail(error.localizedDescription)
}

And you have the crashing one:

// Crash the program
nonThrowingFunc() 

Note

! (as force) is designed for situations that you are pretty sure about the result. good examples:

  • Decoding hardcoded or static JSON
  • Force unwrapping hardcoded values
  • force try interfaces when you know what is the implementation of it at the point
  • etc.

If your function is not pure enough and may fail by passing different arguments, you should consider NOT forcing it and refactor your code to a safer version.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • As I said in the question, this unit test is to verify that the code crashes *only in specific instances*. That is, I'm writing code that fails silently in production, handling the error, but on a simulator I want it to fail loudly so I can catch the error before publishing. But yes, you're correct that Apple does not consider such a case worth support. – Elliot Schrock Oct 18 '21 at 20:03
1

Swift (until & including current 5.5) postulates explicitly that all errors inside non-throwing function MUST be handled inside(!). So swift by-design has no public mechanism to intervene in this process and generates run-time error.

demo

Asperi
  • 228,894
  • 20
  • 464
  • 690
1

You might not even like my answer. But here it goes:

Though I agree that there are plenty of cases where try! is more useful (or event better) than try, I would also argue that they are not meant to be tested because they signify programmer mistakes. A programmer mistake is unplanned. You cannot test something unplanned. If you expect (or suspect) a mistake to happen in production, then you should not be using try! in the first place. To me this is a violation of what I think are standards for programming.

Throwable are added to handle expected mistakes and using try! tells the compiler that you expect a mistake will NEVER happen. For example, when you are parsing a hard-coded value that you know will never fail. So why would you ever need to test a mistake that will never happen?

You can also use an assertionFailure if you want to be rigorous in debug but safe in release.

Jacob
  • 1,052
  • 8
  • 10
0

Runtime errors, like

  • array index out of bounds
  • forcibly unwrapping nil values
  • division by zero

, are considered programming errors, and need precondition checks to reduce the chances they happen in production. The precondition failures are usually caught in the testing phase, as QA people toss the application on all sides, trying to find implementation flaws.

As they are programming errors, you should not need to test them, if indeed the execution of the code reached to a point where the assertion fails, then it means the other code failed to provide valid data. And that's what you should unit test, the other code.

It's best if you avoid the need to have the assertions. !, try!, IOU's, all trap for nil values, so better to avoid these constructs if you're not 100% sure that you'll receive only non-nil values.

A programming error most of the times means that your application reached into an unrecoverable state, and there's little it can be done afterwards, so better let it crash then continue with an invalid state.

Thus, Swift doesn't need to expose an API to handle this kind of scenarios. The existing workarounds, are complicated, fragile, and don't worth to be used in real-life applications.

To conclude:

  • replace the forced unwraps (try! included) with code that can handle nils, and unit test that, or,
  • unit test the caller code, since that's the actual problematic code.

The latter case assumes that the forced unwrap usage is legitimate, as the code expects for the callee to be non-nil, so the burden is moved on the other piece of code, the provider of the value that's being forcefully unwrapped.

Cristik
  • 30,989
  • 25
  • 91
  • 127
  • As I said in the question, this unit test is to verify that the code crashes *only in specific instances*. That is, I'm writing code that fails silently in production, handling the error, but on a simulator I want it to fail loudly so I can catch the error before publishing. But yes, you're correct that Apple does not consider such a case worth support. – Elliot Schrock Oct 18 '21 at 20:02
  • @ElliotSchrock my point is that it's best if you avoid forced unwraps, like `!`, or `try!`, right from the start. The only time it makes sense to use them, is if you're 100% sure that you'll never get nil values, and in that case having a unit test on that code doesn't add much value. – Cristik Oct 19 '21 at 06:05
0

use Generic Function XCTAssertThrowsError in swift for unit testing

Asserts that an expression throws an error.

func XCTAssertThrowsError<T>(_ expression: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, _ errorHandler: (_ error: Error) -> Void = { _ in })

https://developer.apple.com/documentation/xctest/1500795-xctassertthrowserror

Jayesh Patel
  • 938
  • 5
  • 16