Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Most Apps Require Web Services

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

“Welcome to the Real World” — Morpheus

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Dummy Data

Slide 8

Slide 8 text

Build it as if you have the API

Slide 9

Slide 9 text

Agree on an API spec

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Build your own lightweight API

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

The Do it Yourself API

Slide 14

Slide 14 text

Dummy Data + The Do it Yourself API

Slide 15

Slide 15 text

NSURLProtocol and the URL Loading System

Slide 16

Slide 16 text

How does NSURLProtocol help us?

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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 }

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Working with a Web Service is a pain.

Slide 33

Slide 33 text

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