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

Fake It ‘Til You Make It: Staying Productive when Working with Web Services

Fake It ‘Til You Make It: Staying Productive when Working with Web Services

Most apps we build are backed by some form of API. When these APIs are pre-existing or bug free, great! What happens when another team is building the API while you are building the app? How can you build a view with data that doesn’t actually exist? We will review some options for staying productive when working with external teams by exploring:

* Best practices for API development with external teams
* Options for testing nonexistent APIs
* Foundation’s URL Loading System to fake API requests that are in development

Ff90de2297b7e3136fa3c4a971b0b1c5?s=128

Josh Johnson

May 10, 2014
Tweet

Transcript

  1. Fake It ’Til You Make It Staying Productive when Working

    with Web Services Josh Johnson | @jnjosh | jnjosh.com
  2. Most Apps Require Web Services

  3. “My API is perfect and has no bugs!” — A

    really lucky person
  4. “Welcome to the Real World” — Morpheus

  5. You still need to make progress! What can you do?

  6. None
  7. Dummy Data

  8. Build it as if you have the API

  9. Agree on an API spec

  10. Wait? There is no API. How can we build it?

  11. Build your own lightweight API

  12. Sinatra Using tools like Sinatra you can quickly define API

    endpoints to match tha API spec get '/episodes/:episode' do last_modified episodes.first[:published] content_type :json episodes.select { |e| e[:number] == params[:episode].to_i }.to_json end 㱺 GET http://api.testing.com/episodes/8
  13. The Do it Yourself API

  14. Dummy Data + The Do it Yourself API

  15. NSURLProtocol and the URL Loading System

  16. How does NSURLProtocol help us?

  17. Let's download an image! NSString *kittenImageString = @"http://cutekittenimages.com/kittens_cute_kitten.jpg"; NSURL *url

    = [NSURL URLWithString:kittenImageString]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request addValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"]; [NSURLConnection sendAsynchronousRequest:request queue:self.imageQueue completionHandler: ^(NSURLResponse *response, NSData *data, NSError *connectionError) { UIImage *kittenImage = [UIImage imageWithData:data scale:0.0]; [[NSOperationQueue mainQueue] addOperation:[NSBlockOperation blockOperationWithBlock:^{ self.imageView.image = kittenImage; }]]; }];
  18. None
  19. Introducing TWTHasslehoffImageProtocol @import Foundation; @interface TWTHasselhoffImageProtocol : NSURLProtocol @end

  20. Register the Hasselhoff Protocol - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [NSURLProtocol registerClass:[TWTHasselhoffImageProtocol class]]; return YES; }
  21. Implement the Hasselhoff Image Protocol + (BOOL)canInitWithRequest:(NSURLRequest *)request { NSSet

    *validContentTypes = [NSSet setWithArray:@[ @"image/png", @"image/jpg", @"image/jpeg" ]]; return [validContentTypes containsObject:request.allHTTPHeaderFields[@"Content-Type"]]; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; }
  22. Implement the Hasselhoff Image Protocol - (void)startLoading { id<NSURLProtocolClient> client

    = self.client; NSURLRequest *request = self.request; NSDictionary *headers = @{ @"Content-Type": @"image/jpeg" }; NSData *imageData = UIImageJPEGRepresentation([UIImage imageNamed:@"David_Hasselhoff.jpeg"], 1.0); NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headers]; [client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [client URLProtocol:self didLoadData:imageData]; [client URLProtocolDidFinishLoading:self]; } - (void)stopLoading { // Must be implemented }
  23. None
  24. This is great and all, but it is a canned

    example.
  25. HolyMockURL, it's URLMock https://github.com/twotoasters/URLMock

  26. Let's check the weather! - (NSOperation *)fetchTemperatureForLatitude:(NSNumber *)latitude longitude:(NSNumber *)longitude

    success:(void (^)(NSNumber *))successBlock failure:(void (^)(NSError *))failureBlock { NSParameterAssert(latitude && ABS(latitude.doubleValue) <= 90.0); NSParameterAssert(longitude && ABS(longitude.doubleValue) <= 180.0); return [self.operationManager GET:@"weather" parameters:@{ @"lat" : latitude, @"lon" : longitude } success:^(AFHTTPRequestOperation *operation, id response) { if (successBlock) { successBlock([response valueForKeyPath:@"main.temp"]); } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { if (failureBlock) { failureBlock(error); } }]; }
  27. A pretty bad unit test - (void)testFetchTemperatureForLatitudeLongitude { __block NSNumber

    *temperature = nil; [self.APIClient fetchTemperatureForLatitude:@(35.99) longitude:@(-78.9) success:^(NSNumber *kelvins) { temperature = kelvins; } failure:nil]; // Assert that temperature != nil before 2.0s elapse UMKAssertTrueBeforeTimeout(2.0, temperature != nil, @"temperature isn't set in time"); }
  28. Let's use URLMock + (void)setUp { [super setUp]; [UMKMockURLProtocol enable];

    [UMKMockURLProtocol setVerificationEnabled:YES]; } + (void)tearDown { [UMKMockURLProtocol setVerificationEnabled:NO]; [UMKMockURLProtocol disable]; [super tearDown]; } - (void)setUp { [super setUp]; [UMKMockURLProtocol reset]; … }
  29. A better unit test, with URLMock - (void)testFetchTemperatureForLatitudeLongitudeCorrectData { //

    setting up latitude, longitude and tempearture to expect NSURL *temperatureURL = [self temperatureURLWithLatitude:latitude longitude:longitude]; [UMKMockURLProtocol expectMockHTTPGetRequestWithURL:temperatureURL responseStatusCode:200 responseJSON:@{ @"main" : @{ @"temp" : temperature } }]; __block BOOL succeeded = NO; __block BOOL failed = NO; __block NSNumber *kelvins = nil; [self.APIClient fetchTemperatureForLatitude:latitude longitude:longitude success:^(NSNumber *temperatureInKelvins) { succeeded = YES; kelvins = temperatureInKelvins; } failure:^(NSError *error) { failed = YES; }]; UMKAssertTrueBeforeTimeout(1.0, succeeded, @"success block is not called"); UMKAssertTrueBeforeTimeout(1.0, !failed, @"failure block is called"); UMKAssertTrueBeforeTimeout(1.0, [kelvins isEqualToNumber:temperature], @"incorrect temperature"); NSError *verificationError = nil; XCTAssertTrue([UMKMockURLProtocol verifyWithError:&verificationError], @"verification failed"); }
  30. Test for errors, with URLMock - (void)testFetchTemperatureForLatitudeLongitudeError { … NSURL

    *temperatureURL = [self temperatureURLWithLatitude:latitude longitude:longitude]; [UMKMockURLProtocol expectMockHTTPGetRequestWithURL:temperatureURL responseError:[self randomError]]; __block BOOL succeeded = NO; __block BOOL failed = NO; [self.APIClient fetchTemperatureForLatitude:latitude longitude:longitude success:^(NSNumber *temperature) { succeeded = YES; } failure:^(NSError *error) { failed = YES; }]; UMKAssertTrueBeforeTimeout(1.0, !succeeded, @"success block is called"); UMKAssertTrueBeforeTimeout(1.0, failed, @"failure block is not called"); NSError *verificationError = nil; XCTAssertTrue([UMKMockURLProtocol verifyWithError:&verificationError], @"verification failed"); }
  31. URLMock https://github.com/twotoasters/URLMock

  32. Working with a Web Service is a pain.

  33. Thank you! Questions? Josh Johnson | @jnjosh | jnjosh.com https://github.com/twotoasters/URLMock

    https://objectivetoast.com