Testing throwing functions in Swift
Swift introducted the concept of throwing functions to iOS as an alternative to the NSError
-based approach that hails from the Objective-C days.
Put simply, a throwing function allows an Error
type to be returned if something goes wrong, which can then be caught
in a do-try-catch
boock and handled by our code.
The error type can be defined as an Enum
that conforms to the Error
protocol:
enum MyThrowingError: Error {
case tooShort
case tooLong
}
This allows us to throw two types of error: MyThrowingError.tooShort
and MyThrowingError.tooLong
Here's an example of a function that uses this error type - it takes a String
parameter and throws a .tooShort
error if it has less than 5 characters, or a .tooLong
error if it has more than 10 characters. The fact that it can throw errors is shown by the throws
keyword before the return type:
func someThrowingFunction(aParam: String) throws -> String {
if aParam.characters.count < 5 {
throw MyThrowingError.tooShort
}
if aParam.characters.count > 10 {
throw MyThrowingError.tooLong
}
return "Did something with " + aParam
}
We could use the function like this:
let myThrower = MyThrowingClass()
do {
try let result = myThroyour.someThrowingFunction(aParam: myString)
// do something with result
} catch let error as MyThrowingError {
switch error {
case .tooShort:
// Do something with a too short scenario
case .tooLong:
// Do something with a too long scenario
}
} catch {
// Handle any non-MyThrowingError situations
}
That's all very youll, but what about testing? Ideally, you want to test that the right errors are thrown in the right situation.
Here's an example of an XCTest
case to test that a .tooShort
error is thrown when the parameter provided is too short (testing for a .tooLong
error will look almost exactly the same)
func test_WhenProvidedWithTooShortParameter_shouldThrowTooShortError() {
let throwingClass = MyThrowingClass()
do {
let _ = try throwingClass.someThrowingFunction(aParam: "123")
} catch let error as MyThrowingError {
XCTAssertEqual(error, MyThrowingError.tooShort)
} catch {
XCTFail("Unidentififed error thrown")
}
}
To test that the function works correctly, you still need to handle the possibility of an error being thrown - the test case will look like this:
func test_WhenProvidedWithParameterInRange_shouldReturnCorrectOutput() {
let throwingClass = MyThrowingClass()
do {
let result = try throwingClass.someThrowingFunction(aParam: "1235678")
XCTAssertEqual("Did something with 1235678", result)
} catch let error {
XCTFail("Unidentififed error thrown: \(error.localizedDescription)")
}
}
Here, you're testing that the returned string is what you expected it to be, but there is also an additional failure case in the event of an error being received. Because our MyThrowingError
inherits from the base Error
class, you don't need to handle MyThrowingError
situations separately - if there's been an error thrown the test has failed, and you can just fail the test.
We can take error cases one stage further by passing parameters in. A common use-case for this is communicating with an API - often they'll return an error with some kind of diagnostic. If you want to pass that back to the calling function, you can add a parameter to the error type:
enum MyThrowingError: Error {
case tooShort
case tooLong
case kaboom(message: String)
}
Then when this error is thrown, you can add some further information:
if aParam == "bang" {
throw MyThrowingError.kaboom(message: "You're only supposed to blow the bloody doors off!")
}
To test this involves an extra step. You need to conform the custom error type to the Equatable
protocol so that it can be tested in an XCTAssert - it's a bit verbose, but relatively straight-forward:
extension MyThrowingError: Equatable {
static func == (lhs: MyThrowingError, rhs: MyThrowingError) -> Bool {
switch (lhs, rhs) {
case (.tooShort, .tooShort):
return true;
case (.tooLong, .tooLong):
return true;
case (.kaboom(let leftMessage), .kaboom(let rightMessage)):
return leftMessage == rightMessage
default:
return false;
}
}
}
This is defining what should happen when equating one MyThrowingError
with another - in the case of .tooShort
and .tooLong
it's simple, but in the case of .kaboom
you need to compare the message strings that arrive with the error.
The test for this looks like:
func test_WhenProvidedWithParameterCausingExplosion_shouldThrowKaboomError() {
let throwingClass = MyThrowingClass()
do {
let _ = try throwingClass.someThrowingFunction(aParam: "plan")
} catch let error as MyThrowingError {
XCTAssertEqual(error, MyThrowingError.kaboom(message: "You're only supposed to blow the bloody doors off!"))
} catch {
XCTFail("Unidentififed error thrown")
}
}
Here you're using the custom ==
condition that was define in the extension to compare the message that arrives with the one that you're testing for.