Original link: Testing Swift's Errortype:an exploration
Translator: Mmoaay
In this article, we explore the nature of the new Swift error types and observe and test the possibilities and limitations of error handling implementations. Finally, let's end with a description sample and some useful resources
How to AchieveErrorTypeAgreement
If you jump to a location defined in the Swift standard libraryErrorType, we'll see that it doesn't contain too many obvious prerequisites.
protocolErrorType{}
However, when we try to implement itErrorType, we will soon find that there are at least some things that are necessary to satisfy this agreement. For example, if you implement it in an enumeration way, everything is OK.
enum MyErrorEnum : ErrorType {}
But if it is implemented in a structured way, the problem comes.
struct MyErrorStruct : ErrorType {}
Our initial idea might be that, perhaps,ErrorTypea special type, the compiler supports it in a special way, and can only be implemented with Swift's native enumeration. But then you'll remember andNSErrorsatisfy the agreement, so it can't be that special. So our next attempt is toNSObjectimplement this protocol through a derived class.
@objcclass MyErrorClass: ErrorType {}
Unfortunately, it's still not working.
update : Starting with the Xcode 7 Beta 5 release, we may not need to spend additional effort to implement protocols for structs and classesErrorType. Therefore, the following solutions are no longer needed, but remain for reference.
Allows structs and classes to implement theErrorTypeprotocol. (21867608)
How could that be?
ByLLDBfurther investigation, it was found that the protocol had some hidden prerequisites.
(lldb) type lookup ErrorType
protocol ErrorType {
var _domain: Swift.String { get }
var _code: Swift.Int { get }
}
TheNSErrorreason for this definition is clear: it has these properties,ivarssupported by Swift access without the need for dynamic lookups. It is also not clear why Swift's class one citizen enumeration can automatically satisfy this protocol. Maybe there's still some magic inside of it?
If we use our newly acquired knowledge to implement the structure and the class, everything will be OK.
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 other thrown errors
Historically, the patterns in Apple's frameworkNSErrorPointerhave played an important role in error handling. This has been made easier by the objective-c API and Swift's perfect convergence. Errors that determine the domain are exposed as enumerations, so you can simply drop them without using the "magic numbers". But what if you need to catch a bug that's not exposed?
Let's say we need to deserialize a JSON string, but we're not sure it's valid. We will useFoundationNSJSONSerializationit to do this thing. When we pass it an exception to the JSON string, it throws an error code of 3840 .
Of course, you can catch it with generic errors and then manually check_domainand_codedomain, but we have a more elegant alternative.
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 is to introduce a generic error structure that satisfies the protocol through the methods we discovered earlierErrorType. When we implement the pattern matching operator for it~=, we cando … catchuse it in the branch.
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")
}
However, in the current situation,NSCocoaErrorThis helper class contains a number of static methods that define the various errors.
What is called error hereNSCocoaError.PropertyListReadCorruptErroris not so obvious, but it does have the error code we need. Whether you're capturing errors through a standard library or a third-party framework, if you have something like this, you need to rely on a given constant instead of defining it yourself again.
let json : NSString = "{"
let data = json.dataUsingEncoding(NSUTF8StringEncoding)
do {
let object : AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(object)
} catch NSCocoaErrorDomain {
print("Invalid format")
}
Custom error Handling Writing specifications
So what do we do next? After charging our code with Swift's error handling, whether we are replacing all those distractingNSErrorpointer assignments or stepping back to the type in the functional paradigmResult, we need to make sure that the errors we expect are thrown correctly. The boundary value is always the most interesting scenario for testing, and we want to make sure that all the protections are in place and throw the appropriate error when appropriate.
Now we have some basic understanding of how this error type works at the bottom, and we have some ideas on how to let it follow our wishes when testing. So let's show a small test case: We have a banking App, and then we want to model the actual activity in the business logic. We created the structure account, which represents the bank accounts, which contains an interface that exposes a method for trading within the budget range.
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 outcome
} catch {
XCTFail ("Catched error \" \ (error) \ ", but not the expected: \" \ (Error.TransactionExceedsFunds) \ "")
}
}
}
Now imagine that we have more methods and more error scenarios. In test-oriented development, we want to test them all to ensure that all errors are correctly dropped-and we certainly don't want to move the money to the wrong place! Ideally, we don't want to repeat this in all the test codedo-catch. To implement an abstraction, we can put it in a higher-order function.
/// 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 outcome.
} catch {
XCTFail ("Catched error \" \ (error) \ ","
+ "but not from the expected type"
+ "\" \ (expectedError) \ ".")
}
}
This code can be used in this way:
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))
}
}
However, you may find that the expected parameterization errorsNonPositiveTransactionNotAllowedare more than the parameters used hereamount. How do we make strong assumptions about the error scenarios and their associated values? First, we can implement the protocol for the error typeEquatable, and then add a check on the number of parameters for the relevant scene in the implementation of the equality operator.
/// Extend our error type and implement `Equatable`.
/// This must be done for each specific type,
/// instead of uniform implementation for `ErrorType`.
extension Error: Equatable {}
/// Implement the `==` operator in the required way for the 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 to return false for the various combined scenarios in the default scenario.
// By comparing domain and code, we can guarantee
// once we add other error scenarios, if this scenario has corresponding value
// I just need to go back and modify the implementation of Equatable
return lhs._domain == rhs._domain
&& lhs._code == rhs._code
}
}
The next step is to makeAssertThrowsure that there are reasonable mistakes. You might think that we can extend an existing implementation byAssertThrowsimply checking that the expected error is justified. But the unfortunate drops are useless:
The "equatable" protocol can only be used as a generic constraint because it needs to satisfy the requirements of self or association type
Instead, we can overload the first parameter by one more generic parameterAssertThrow.
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 then, as expected, our test eventually returned to failure.
Note that the assertion implementation of the latter makes a strong assumption of the type of error.
Do not use the "catch other thrown errors" method, because it cannot match the type compared to the current method. Maybe it's just an objection, not a rule.
A few useful resources
At Realm, we use Xctest and our own sub-XCTestCaseclasses and combine some predictors to meet our specific needs. Happily, if you want to use this code, you don't need to copy-paste, and you don't need to reinvent the wheel. The error predictor is available on GitHub's Catchingfire project, and if you're not aXCTestbig fan of predictor style, you might prefer a nimble-like test framework that can also provide testing support.
Be happy to test oh ~
Explore: Test ErrorType in Swift