$30 off During Our Annual Pro Sale. View Details »

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

Josh Johnson

May 10, 2014
Tweet

More Decks by Josh Johnson

Other Decks in Programming

Transcript

  1. Fake It ’Til You Make It
    Staying Productive when Working with Web
    Services
    Josh Johnson | @jnjosh | jnjosh.com

    View Slide

  2. Most Apps Require Web Services

    View Slide

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

    View Slide

  4. “Welcome to the Real World”
    — Morpheus

    View Slide

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

    View Slide

  6. View Slide

  7. Dummy Data

    View Slide

  8. Build it as if
    you have the API

    View Slide

  9. Agree on an API spec

    View Slide

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

    View Slide

  11. Build your own lightweight API

    View Slide

  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

    View Slide

  13. The Do it Yourself API

    View Slide

  14. Dummy Data
    +
    The Do it Yourself API

    View Slide

  15. NSURLProtocol
    and the
    URL Loading System

    View Slide

  16. How does NSURLProtocol help us?

    View Slide

  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;
    }]];
    }];

    View Slide

  18. View Slide

  19. Introducing TWTHasslehoffImageProtocol
    @import Foundation;
    @interface TWTHasselhoffImageProtocol : NSURLProtocol
    @end

    View Slide

  20. Register the Hasselhoff Protocol
    - (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    [NSURLProtocol registerClass:[TWTHasselhoffImageProtocol class]];
    return YES;
    }

    View Slide

  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;
    }

    View Slide

  22. Implement the Hasselhoff Image Protocol
    - (void)startLoading
    {
    id 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
    }

    View Slide

  23. View Slide

  24. This is great and all, but it is a canned example.

    View Slide

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

    View Slide

  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);
    }
    }];
    }

    View Slide

  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");
    }

    View Slide

  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];

    }

    View Slide

  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");
    }

    View Slide

  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");
    }

    View Slide

  31. URLMock
    https://github.com/twotoasters/URLMock

    View Slide

  32. Working with a Web Service is a pain.

    View Slide

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

    View Slide