In this post, we explore the nature of Swift’s new error type, looking at the possibilities and restrictions for testing error handling implementations. We end with an illustrative example, and some helpful resources.
How to Implement the ErrorType
Protocol
If we jump to the definition of the ErrorType
protocol in the Swift standard library, we see that it doesn’t contain many obvious requirements.
protocol ErrorType {
}
However, if we try to implement ErrorType
, it soon becomes obvious that at least something is required to satisfy this protocol. For example, if we implement it as an enum, everything works fine.
enum MyErrorEnum : ErrorType {
}
But if we try to implement it as struct, things stop working.
struct MyErrorStruct : ErrorType {
}
Our first thought might be, perhaps, ErrorType
is a special type, supported in a special way by the compiler, and it can only be implemented from Swift’s native enumeration. But then, you might also remember that NSError
fulfills this protocol as well, so it can’t be that special. As our next attempt, let’s try to implement the protocol from an NSObject
descendant.
@objc class MyErrorClass: ErrorType {
}
Unfortunately, that still doesn’t work.
Update: Since Xcode 7 beta 5, it’s possible to implement ErrorType
from structs and classes without further effort. So the workaround described below isn’t necessary anymore, but kept for reference.
Structs and classes are now allowed to conform to ErrorType. (21867608)
How is This Possible?
Further investigation via LLDB reveals that the protocol has some hidden requirements.
(lldb) type lookup ErrorType
protocol ErrorType {
var _domain: Swift.String { get }
var _code: Swift.Int { get }
}
It becomes clear how NSError
fulfills this definition: it has these properties, backed by ivars, which Swift can access without dynamic dispatch. What is still unclear is how Swift’s first class enums can automatically fulfill this protocol. Perhaps there is still some magic involved?
If we try to implement structs or classes with our newly obtained knowledge, things start working.
struct MyErrorStruct : ErrorType {
let _domain: String
let _code: Int
}
class MyErrorClass : ErrorType {
let _domain: String
let _code: Int
init(domain: String, code: Int) {
_domain = domain
_code = code
}
}
Catch the Errors Thrown by Others
Historically, Apple’s frameworks make significant use of the NSErrorPointer
pattern to provide fallible methods. With the excellent bridging of Objective-C APIs to Swift, these have been a lot easier to use. Errors from certain domains are exposed as enums, which makes them a lot easier to catch without using any magic numbers. But let’s assume you want to catch an error which is not exposed, how could you do that?
Let’s assume we want to deserialize a JSON string, but can’t be sure whether it is valid or not. We will use Foundation’s NSJSONSerialization
for that. When we feed it with malformed JSON it throws an error with code 3840
. Sure, you could catch it as any generic error and then manually check the fields _domain
and _code
, but there are more elegant alternatives.
let json : NSString = "{"
let data = json.dataUsingEncoding(NSUTF8StringEncoding)
do {
let object : AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(object)
} catch let error {
if error._domain == NSCocoaErrorDomain
&& error._code == 3840 {
print("Invalid format")
} else {
throw error
}
}
Another alternative involves introducing a generic error struct, which fulfills the ErrorType
protocol in the way we discovered above. When we implement the pattern matching operator ~=
for this, we can use it in our do … catch
branches.
struct Error : ErrorType {
let domain: String
let code: Int
var _domain: String {
return domain
}
var _code: Int {
return code
}
}
func ~=(lhs: Error, rhs: ErrorType) -> Bool {
return lhs._domain == rhs._domain
&& rhs._code == rhs._code
}
let json : NSString = "{"
let data = json.dataUsingEncoding(NSUTF8StringEncoding)
do {
let object : AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(object)
} catch Error(domain: NSCocoaErrorDomain, code: 3840) {
print("Invalid format")
}
But in this case, there is also NSCocoaError
, which is a helper class with static methods defined for various errors. That the error raised here is named NSCocoaError.PropertyListReadCorruptError
isn’t perhaps so obvious, but it has the code we’re looking for. Whether you try to catch errors from the standard library or third-party frameworks, if there is anything like this, you should try to rely on the provided constants instead of defining them yourself.
let json : NSString = "{"
let data = json.dataUsingEncoding(NSUTF8StringEncoding)
do {
let object : AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(object)
} catch NSCocoaErrorDomain {
print("Invalid format")
}
Write Specs Against Your Own Error Handling
So what’s next? After we’ve spiced our code with Swift’s error handling, either by replacing all of those distracting NSError
pointer assignments, or by taking a step back from the functional paradigm’s Result
type, we have to make sure that our errors are thrown when we expect them. The edge cases are always the most interesting cases for tests, we want to make sure that all our guards are in place and throw their errors when they are supposed to.
Now that we have some basic understanding of how this error type works under the hood, we also have some idea of how to bend it to our will when it comes to testing.
Let’s introduce a small example to illustrate a test case: we have a banking app, and we want to model real-world activities in our business logic. We create a struct Account
, which represents a bank account. It has a public interface which exposes methods to make transactions within the available budget.
public enum Error : ErrorType {
case TransactionExceedsFunds
case NonPositiveTransactionNotAllowed(amount: Int)
}
public struct Account {
var fund: Int
public mutating func withdraw(amount: Int) throws {
guard amount < fund else {
throw Error.TransactionExceedsFunds
}
guard amount > 0 else {
throw Error.NonPositiveTransactionNotAllowed(amount: amount)
}
fund -= amount
}
}
class AccountTests {
func testPreventNegativeWithdrawals() {
var account = Account(fund: 100)
do {
try account.withdraw(-10)
XCTFail("Withdrawal of negative amount succeeded, but was expected to fail.")
} catch Error.NonPositiveTransactionNotAllowed(let amount) {
XCTAssertEqual(amount, -10)
} catch {
XCTFail("Catched error \"\(error)\", but not the expected: \"\(Error.NonPositiveTransactionNotAllowed)\"")
}
}
func testPreventExceedingTransactions() {
var account = Account(fund: 100)
do {
try account.withdraw(101)
XCTFail("Withdrawal of amount exceeding funds succeeded, but was expected to fail.")
} catch Error.TransactionExceedsFunds {
// Expected.
} catch {
XCTFail("Catched error \"\(error)\", but not the expected: \"\(Error.TransactionExceedsFunds)\"")
}
}
}
Now imagine that we have some further methods, and more error cases. In a test-driven manner, we want to test them all, to make sure that all the errors are thrown correctly — we don’t want to move money to the wrong place! Ideally, we don’t really want to repeat this do-catch pattern throughout our tests. Applying an abstraction, we can put it in a higher-order function.
/// Implement pattern matching for ErrorType
public func ~=(lhs: ErrorType, rhs: ErrorType) -> Bool {
return lhs._domain == rhs._domain
&& lhs._code == rhs._code
}
func AssertThrow<R>(expectedError: ErrorType, @autoclosure _ closure: () throws -> R) -> () {
do {
try closure()
XCTFail("Expected error \"\(expectedError)\", "
+ "but closure succeeded.")
} catch expectedError {
// Expected.
} catch {
XCTFail("Catched error \"\(error)\", "
+ "but not from the expected type "
+ "\"\(expectedError)\".")
}
}
This can be used by:
class AccountTests : XCTestCase {
func testPreventExceedingTransactions() {
var account = Account(fund: 100)
AssertThrow(Error.TransactionExceedsFunds, try account.withdraw(101))
}
func testPreventNegativeWithdrawals() {
var account = Account(fund: 100)
AssertThrow(Error.NonPositiveTransactionNotAllowed(amount: -10), try account.withdraw(-20))
}
}
But as you might have caught, the parameterized error NonPositiveTransactionNotAllowed
in the expectation holds a different amount than the parameter which should be used here. How can we make strong assumptions about not only the error case, but also their associated values? First, we can implement the protocol Equatable
for our error type, and add a check for the amount in the relevant case of the equality operator implementation.
/// Extend our Error type to implement `Equatable`.
/// This must be done per individual concrete type
/// and can't be done in general for `ErrorType`.
extension Error : Equatable {}
/// Implement the `==` operator as required by protocol `Equatable`.
public func ==(lhs: Error, rhs: Error) -> Bool {
switch (lhs, rhs) {
case (.NonPositiveTransactionNotAllowed(let l), .NonPositiveTransactionNotAllowed(let r)):
return l == r
default:
// We need a default case to return false for different case combinations.
// By falling back to domain and code based comparison, we ensure that
// as soon as we add additional error cases, we have to revisit only the
// Equatable implementation, if the case has an associated value.
return lhs._domain == rhs._domain
&& lhs._code == rhs._code
}
}
The next step is making AssertThrow
aware that there are Errors which are Equatable. As you might guess, we could extend our existing AssertThrow
implementation, with a simple check for whether the expected error is Equatable. Unfortunately that doesn’t work:
Protocol ‘Equatable’ can only be used as a generic constraint because it has Self or associated type requirements
Instead, we can overload AssertThrow
by a case with a further generic parameter for the first argument.
func AssertThrow<R, E where E: ErrorType, E: Equatable>(expectedError: E, @autoclosure _ closure: () throws -> R) -> () {
do {
try closure()
XCTFail("Expected error \"\(expectedError)\", "
+ "but closure succeeded.")
} catch let error as E {
XCTAssertEqual(error, expectedError,
"Catched error is from expected type, "
+ "but not the expected case.")
} catch {
XCTFail("Catched error \"\(error)\", "
+ "but not the expected error "
+ "\"\(expectedError)\".")
}
}
And with that our test finally fails, as expected. Note that the latter assert implementation now makes strong assumptions about the type of the error. You can’t use the approach shown above, under “Catch The Errors Thrown by Others”, in combination with this approach, as the types won’t match. Likely, this will remain the exception rather than the rule.
Some Helpful Resources
At Realm, we use XCTest and our own home-grown XCTestCase
subclass together with a few expecters, which suits our very specific needs. Gladly, you don’t need to copy-paste a bunch of code to make use of this, and you don’t need to reinvent everything yourself. The error expecter is available on GitHub as CatchingFire. If you are not a big fan of the style of expecters that XCTest
brings, then you’d probably be more interested in test frameworks like Nimble, which will bring support for this as well.
Happy testing!
Receive news and updates from Realm straight to your inbox