Handling errors

Using try! instead of try in the call to JSONObjectWithData(_:options:), we tell the compiler: "Trust me on this: This method will never fail.". Let's write a test that feeds in wrong data and asserts that an error is thrown:

func testLogin_ThrowsErrorWhenJSONIsInvalid() {
    
    var theError: ErrorType?
    let completion = { (error: ErrorType?) in
        theError = error
    }
    sut.loginUserWithName("dasdom",
        password: "1234",
        completion: completion)
    
    let responseData = NSData()
    mockURLSession.completionHandler?(responseData, nil, nil)
    
    XCTAssertNotNil(theError)
}

In the test, we call the completion handler with an empty data object.

Run the tests. The implementation code crashes because the deserialization fails and throws an error. Let's change the code that it handles the thrown error correctly. Replace the contents of the completion handler with this:

do {
    let responseDict = try NSJSONSerialization.JSONObjectWithData(data!,
        options: [])
    
    let token = responseDict["token"] as! String
    self.keychainManager?.setPassword(token,
        account: "token")
} catch {
    completion(error)
}

With this code, we can catch the error if there is one, and pass it to the completion block of the login method. Run the tests. All the tests pass again.

Next, we need to make sure that the implementation code can handle a nil data value. Add the following test:

func testLogin_ThrowsErrorWhenDataIsNil() {
    
    var theError: ErrorType?
    let completion = { (error: ErrorType?) in
        theError = error
    }
    sut.loginUserWithName("dasdom",
        password: "1234",
        completion: completion)
    
    mockURLSession.completionHandler?(nil, nil, nil)
    
    XCTAssertNotNil(theError)
}

Run the test. It crashes again in the completion handler of the data task. The reason for this crash is that we try to force-unwrap data value and it is nil this time.

Replace the contents of the completion handler of the data task with this:

if let theData = data {
    do {
        let responseDict = try NSJSONSerialization.JSONObjectWithData(theData,
            options: [])
        
        let token = responseDict["token"] as! String
        self.keychainManager?.setPassword(token,
            account: "token")
    } catch {
        completion(error)
    }
}

In this code, we use if let to conditionally unwrap the data if it is not nil.

Run the tests. The crash is gone but the test still fails. The reason for this is that we do not call the completion block of the login method in case the token cannot be extracted from the response data. To make the test pass, we need to define the errors we want to throw. Add the following enum to the end of APIClient.swift:

enum WebserviceError : ErrorType {
    case DataEmptyError
}

Now, add the following else clause in the completion handler of the data task:

if let theData = data {
    // …
} else {
    completion(WebserviceError.DataEmptyError)
}

Run the tests. All the tests pass and there is nothing to refactor.

There is one error left that we need to handle. The completion handler of the data task is called with an error parameter. The web service returns any error that has occurred on the server side in this parameter. Our code has to handle this error. Add the following test to make sure that the implementation handles the error when it is set:

func testLogin_ThrowsErrorWhenResponseHasError() {
    
    var theError: ErrorType?
    let completion = { (error: ErrorType?) in
        theError = error
    }
    sut.loginUserWithName("dasdom",
        password: "1234",
        completion: completion)
    
    let responseDict = ["token" : "1234567890"]
    let responseData = try! NSJSONSerialization.dataWithJSONObject(responseDict,
        options: [])
    let error = NSError(domain: "SomeError", code:
        1234, userInfo: nil)
    mockURLSession.completionHandler?(responseData, nil, error)
    
    XCTAssertNotNil(theError)
}

Note that we call the completion handler with valid response data. If we pass in nil as data in this test, it would already pass, even though we haven't written the code to handle the response error.

To make this test pass, add the ResponseError case to the WebserviceError enum, and add the following code to the beginning of the completion handler of the data task:

if error != nil {
    completion(WebserviceError.ResponseError)
    return
}

Run the tests. All the tests pass and there is nothing to refactor.

There are still some tests and implementations for the APIClient class that are missing. We could add tests to fetch an item from and post an item to the web service; for example, to make it possible to access the to-do items from a web application. We won't add the tests in this book because they would look similar to the tests we have already written. But you should add the tests yourself to practice the TDD workflow.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.95.107