22

I'm writing an iPhone application that needs to get some data from a web server. I'm using NSURLConnection to do the HTTP request, which works well, but I'm having trouble unit testing my code in the case where the response has an HTTP error code (like 404 or 500).

I'm using GTM for unit testing and OCMock for mocking.

When the server returns an error, the connection does not call connection:didFailWithError: on the delegate, but calls connection:didReceiveResponse:, connection:didReceiveData:, and connectionDidFinishLoading: instead. I'm currently checking the status code on the response in connection:didReceiveResponse: and calling cancel on the connection when the status code looks like an error to prevent connectionDidFinishLoading: from being called, where a successful response would be reported.

Providing a static stubbed NSURLConnection is simple, but I want my test to change it's behaviour when one of the mock connection's methods is called. Specifically, I want the test to be able to tell when the code has called cancel on the mock connection, so the test can stop calling connection:didReceiveData: and connectionDidFinishLoading: on the delegate.

Is there a way for tests to tell if cancel has been called on the mock object? Is there a better way to test code that uses NSURLConnection? Is there a better way to handle HTTP error statuses?

Will Harris
  • 21,597
  • 12
  • 64
  • 64

1 Answers1

43

Is there a better way to handle HTTP error statuses?

I think you are on the right track. I use something similar to the following code, which I found here:

if ([response respondsToSelector:@selector(statusCode)])
{
    int statusCode = [((NSHTTPURLResponse *)response) statusCode];
    if (statusCode >= 400)
    {
        [connection cancel];  // stop connecting; no more delegate messages
        NSDictionary *errorInfo
          = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:
            NSLocalizedString(@"Server returned status code %d",@""),
            statusCode]
                                        forKey:NSLocalizedDescriptionKey];
        NSError *statusError
          = [NSError errorWithDomain:NSHTTPPropertyStatusCodeKey
                                code:statusCode
                            userInfo:errorInfo];
        [self connection:connection didFailWithError:statusError];
    }
}

This cancels the connection, and calls connection:didFailWithError: in order to make http error codes behave exactly the same as any other connection error.

e.James
  • 116,942
  • 41
  • 177
  • 214
  • 1
    Works great. Slight issue: `NSHTTPPropertyStatusCodeKey` is deprecated. – Johan Kool Dec 04 '09 at 19:00
  • 3
    You can use any string, for example @"Error", instead of NSHTTPPropertyStatusCodeKey – Mihir Mathuria May 20 '11 at 18:22
  • Yeah, this does not look like a correct use of `NSHTTPPropertyStatusCodeKey`. Cf. the NSURL class reference: http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSURL_Class/Reference/Reference.html%23//apple_ref/occ/instm/NSURL/relativePath. Also, if you are Xcode 4.4 or later, you can probably shorten the dictionary code to `@{NSLocalizedDescriptionKey : [NSHTTPURLResponse localizedStringForStatusCode:statusCode]}`. – Clay Bridges Aug 28 '12 at 20:36
  • @Clay Bridges: It worked back in 2009, but I haven't kept up to date with any changes since then. I assume that by now there is a better way. Any ideas? – e.James Aug 28 '12 at 23:32
  • @e.James As @Mihir Mathuria said, any string will work as an error domain. With some import finagling, `NSHTTPPropertyStatusCodeKey` **works** fine. However: (1) error domain is not its intended use, which per the docs is "Pass these keys to NSURLHandle's propertyForKeyIfAvailable: to request specific data." (2) it does not exist in iOS docs, and the Mac docs declare it "Deprecated in OS X v10.4." Bluntly, using it there was always pointless & misleading, and now its likely to eventually fail to compile. I say this having otherwise copied your code into a shipping app, so... with love! :) – Clay Bridges Aug 29 '12 at 15:54
  • 2
    @Clay Bridges: I feel the love `:)` thank you. So the `domain:` argument is really just a string constant used so that the error handler can get some idea about what has gone wrong. It doesn't have to be `NSHTTPPropertyStatusCodeKey` for this to work, and in fact, it *shoudln't* be, because that key is deprecated. Do I have it right? – e.James Aug 30 '12 at 15:10
  • Mostly. *Shouldn't be* because (1) that key is deprecated (2) should never have been used for an error domain anyway. The only error domain constants that I know about can be found in `NSError.h`, e.g. `NSCocoaErrorDomain`. Otherwise, IIUC, you're better off using a straightforward @"error" or something like it. Using the offending constant for a (very) unintended purpose is misleading and doesn't buy you anything. – Clay Bridges Aug 30 '12 at 17:40
  • @e.James: Is calling connection: didFailWithError: of our own a good approach? I thought it's a delegate method which is supposed to be called by framework. – Rashmi Ranjan mallick Aug 17 '15 at 07:29
  • Does all the HTTP status codes less than 400 point to a successful url connection? – Rashmi Ranjan mallick Aug 17 '15 at 07:31
  • @RashmiRanjanmallick: No: the standard says only the 2xx codes mean "success": http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html. Although I have never seen a 1xx or 3xx code in my (limited) experience. – e.James Oct 06 '15 at 03:12