Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Consuming Web APIs, the TDD way

Consuming Web APIs, the TDD way

Apps consuming web APIs are very common these days, but how often do we get them right?

The way our apps communicate with services across the wire has a tremendous impact in the final user experience. This talk will cover what it takes to bring the best experience to end users by properly consuming web APIs.

We will talk about creating and consuming a web service, following best practices in API design and we will explore different possibilities on how to design API clients. We'll take a look at major concerns like networking, parsing, caching, and error handling, while emphasizing testing and driving the design of our clients with TDD. We'll discuss best practices, tools and tricks to get the best from an API and bring it to our users.

80cab97739e3c37a3ca355f29fa3b9e9?s=128

Luis Solano

June 27, 2014
Tweet

More Decks by Luis Solano

Other Decks in Programming

Transcript

  1. Consuming Web APIs, the TDD way

  2. Luis Solano @luisobo

  3. None
  4. Good practices for networking code Quick intro to Test-driven development

    Tackle real life testing scenarios Underlaying principles of TDD Error handling
  5. API Client design

  6. Sockets HTTP Business logic Application

  7. Sockets HTTP Business logic Application

  8. Sockets HTTP Business logic Application API Client Abstraction

  9. API Client Abstraction HTTP Content type negotiation HTTP error handling

    Authentication Parsing Caching Background Business error handling Business logic Retries State
  10. Test-driven development

  11. Test Implementation Refactor

  12. Design tool Tests are a nice side effect of TDD

    Short feedback loop Enable us to modify our code
  13. GET /dogs.json { "dogs": [{ "name": "perro", "color": "brown" },{

    "name": "tomas", "color": "black" }] }
  14. Our first test

  15. it(@"retrieves dogs", ^{ ! ! ! ! ! ! !

    ! ! }); ! [[LSBDogCare new] allDogs:^(NSArray *dogs) { ! } failure:^(NSError *error) { }]; [[expectFutureValue(capturedDogs) shouldEventually] equal:@[@{@"name":@"perro",@"color":@"brown"}, @{@"name":@"tomas",@"color":@"black"} ]]; __block NSArray *capturedDogs = nil; ! capturedDogs = dogs;
  16. @implementation LSBDogCare ! - (void)allDogs:(void(^)(NSArray *dogs))success failure:(void(^)(NSError *error))failure { !

    ! ! ! ! ! ! ! ! ! ! ! ! ! ! } ! @end NSURL *url = [NSURL URLWithString:@"http://api.example.com/dogs.json"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; AFJSONRequestOperation *op; op = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSDictionary *JSON) { ! ! } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *requestError, id JSON) { }]; success(JSON[@"dogs"]); [op start];
  17. it(@"retrieves dogs", ^{ ! __block NSArray *capturedDogs = nil; [[LSBDogCare

    new] allDogs:^(NSArray *dogs) { capturedDogs = dogs; } failure:^(NSError *error) { }]; [[expectFutureValue(capturedDogs) shouldEventually] equal:@[@{@"name":@"perro",@"color":@"brown"}, @{@"name":@"tomas",@"color":@"black"} ]]; ! });
  18. Isolate tests from undeterministic and slow depencencies.

  19. it(@"retrieves dogs", ^{ ! ! ! ! ! ! !

    ! ! ! ! ! ! ! ! ! ! ! ! ! ! __block NSArray *capturedDogs = nil; [[LSBDogCare new] allDogs:^(NSArray *dogs) { capturedDogs = dogs; } failure:^(NSError *error) { ! }]; ! [[expectFutureValue(capturedDogs) shouldEventually] equal:@[@{@"name":@"perro",@"color":@"brown"}]]; ! }); AFJSONRequestOperation *mockOp = [AFJSONRequestOperation mock]; [AFJSONRequestOperation stub:@selector(JSONRequestOperationWithRequest:success:failure:) withBlock:^id(NSArray *params) { ! ! return mockOp; }]; [mockOp stub:@selector(start) withBlock:^id(NSArray *params) { capturedSuccess(capturedRequest, [NSHTTPURLResponse mock], @{@"dogs":@[@{@"name":@"perro",@"color":@"brown"}]}); return nil; }]; __block NSURLRequest *capturedRequest; __block void (^capturedSuccess)(NSURLRequest *request, NSHTTPURLResponse *response, id JSON); capturedRequest = params[0]; capturedSuccess = params[1];
  20. None
  21. Don’t couple test with implementation details

  22. Tests exist to let us modify our code.

  23. Test Implementation Refactor What my coworkers think I do

  24. Test Implementation Refactor Change a test Change implementation Shit I

    forgot to stub that What I actually do
  25. $ git reset --hard

  26. Don’t modify test and implementation at the same time

  27. it(@"retrieves dogs", ^{ ! ! ! ! ! ! !

    ! ! ! ! ! ! ! ! ! ! ! ! ! ! __block NSArray *capturedDogs = nil; [[LSBDogCare new] allDogs:^(NSArray *dogs) { capturedDogs = dogs; } failure:^(NSError *error) { ! }]; ! [[expectFutureValue(capturedDogs) shouldEventually] equal:@[@{@"name":@"perro",@"color":@"brown"}]]; ! }); AFJSONRequestOperation *mockOp = [AFJSONRequestOperation mock]; [AFJSONRequestOperation stub:@selector(JSONRequestOperationWithRequest:success:failure:) withBlock:^id(NSArray *params) { ! ! return mockOp; }]; [mockOp stub:@selector(start) withBlock:^id(NSArray *params) { capturedSuccess(capturedRequest, [NSHTTPURLResponse mock], @{@"dogs":@[@{@"name":@"perro",@"color":@"brown"}]}); return nil; }]; __block NSURLRequest *capturedRequest; __block void (^capturedSuccess)(NSURLRequest *request, NSHTTPURLResponse *response, id JSON); capturedRequest = params[0]; capturedSuccess = params[1];
  28. None
  29. it(@"retrieves dogs", ^{ ! ! ! ! ! ! __block

    NSArray *capturedDogs = nil; [[LSBDogCare new] allDogs:^(NSArray *dogs) { capturedDogs = dogs; } failure:^(NSError *error) { ! }]; ! [[expectFutureValue(capturedDogs) shouldEventually] equal:@[@{@"name":@"perro",@"color":@"brown"}, @{@"name":@"tomas",@"color":@"black"}]]; }); stubRequest(@"GET", @"http://api.example.com/dogs.json") .andReturn(200) .withHeaders(@{ @"Content-Type": @"application/json;charset=utf-8" }) .withBody([@{@"dogs":@[ @{@"name":@"perro",@"color":@"brown"}, @{@"name":@"tomas",@"color":@"black"}] } JSONString]);
  30. context(@"when retrieving dogs fail because access was revoked", ^{ it(@"reports

    an error", ^{ ! ! ! ! ! ! ! ! ! ! ! ! ! }); }); __block NSError *capturedError; [[LSBDogCare new] allDogs:^(NSArray *dogs) { } failure:^(NSError *error) { capturedError = error; }]; [[expectFutureValue(capturedError) shouldEventually] equal:[NSError errorWithDomain:@"com.luisobo.dogcare" code:23 userInfo:@{ NSLocalizedDescriptionKey: @"uh uh uh, you didn't say the magic word" }]]; stubRequest(@"GET", @"http://api.example.com/dogs.json") .andReturn(401);
  31. - (void)allDogs:(void(^)(NSArray *dogs))success failure:(void(^)(NSError *error))failure { ! // ... op

    = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSDictionary *JSON) { success(JSON[@"dogs"]); } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { ! ! ! ! ! ! ! ! ! ! ! ! }]; ! [op start]; } if (response.statusCode == 401) { error = [NSError errorWithDomain:@"com.luisobo.dogcare" code:23 userInfo:@{ NSLocalizedDescriptionKey: @"uh uh uh, you didn't say the magic word" }]; } failure(error);
  32. context(@"when the request fails because there is no internet", ^{

    it(@"reports an error", ^{ ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! }); }); __block NSError *capturedError; [[LSBDogCare new] allDogs:^(NSArray *dogs) { } failure:^(NSError *error) { capturedError = error; }]; [[expectFutureValue(capturedError) shouldEventually] equal:[NSError errorWithDomain:@"com.luisobo.dogcare" code:446 userInfo:@{ NSLocalizedDescriptionKey: @"eeeer, check your internet dumbass" }]]; stubRequest(@"GET", @"http://api.example.com/dogs.json") .andFailWithError([NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotConnectToHost userInfo:nil]);
  33. None
  34. Test Implementation Refactor

  35. https://github.com/luisobo/Nocilla

  36. Error handling The operation could not be completed. (com.facebook.sdk error

    2)
  37. NSError = domain + code

  38. 1.Developer screw-ups 2.Automatic recoverable errors (e.g. reauthorize) 3.User recoverable errors

    (e.g. validation, ask for more permissions) 4.Known non-recoverable errors. (e.g. server is down, rate limiting) 5.“How the hell did I get here?” errors
  39. Group errors based on how the client is going to

    recover from them
  40. None
  41. None
  42. None
  43. None
  44. None
  45. @interface NSError (FacebookAPI) ! ! ! ! ! ! !

    @end + (instancetype)facebookAPIErrorWithResponse:(NSDictionary *)response; ! - (BOOL)isFacebookAPIError; // com.facebook.api ? ! - (BOOL)shouldRecoverByReauthorizing; - (BOOL)shouldRecoverByRequestingMorePermissions; - (BOOL)shouldRecoverByNotifyingUser;
  46. failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { !

    ! ! ! ! ! ! ! ! ! } NSError *facebookError = [NSError facebookAPIErrorWithResponse:JSON]; if ([facebookError shouldRecoverByReauthorizing]) { // Reauthorize } else if ([facebookError shouldRecoverByRequestingMorePermissions]) { // Request more permissions } else if ([facebookError shouldRecoverByNotifyingUser]) { }
  47. @interface NSError (FacebookSDK) + (instancetype)facebookSDKErrorWithError:(NSError *)error; ! - (BOOL)isFacebookSDKError; //

    com.facebook.sdk ? ! // ... Same deal @end
  48. HTTP Business logic com.facebook.api com.facebook.sdk request like POST /likes

  49. failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) { !

    ! ! ! ! ! ! ! ! ! ! ! } NSError *facebookError = [NSError facebookAPIErrorWithResponse:JSON]; if ([facebookError shouldRecoverByReauthorizing]) { // Reauthorize } else if ([facebookError shouldRecoverByRequestingMorePermissions]) { // Request more permissions } else if ([facebookError shouldRecoverByNotifyingUser]) { } else { failure([NSError genericError]); } failure([NSError facebookSDKErrorWithError:error]); // Ensure consistent state // Log facebookError in a remote service
  50. Build an abstraction on top of of an stateless wrapper

    Isolate test from undeterministic and slow dependencies. Don’t modify implementation and tests at the same time. Don’t couple test with implementation details Group errors by recover strategy
  51. Thanks Say hi @luisobo