📜 ⬆️ ⬇️

Simple mocking of requests to the server + unit-testing of block callbacks in Objective-C

What for

1. Why replace the server response?
I have always been and will be a supporter of the approach, when everyone is responsible for his domain domain. And let's say, if the server with the API fails, then the back-end unit tests, and not the fallen tests of my iOS application, should detect this.

2. Why use blocks, why not target-action, delegation, and so on?
This is a personal preference of everyone, in almost all situations, the objects I develop will have block callbacks and not call delegate methods. For me it works and I have not experienced any special problems with this approach. In the end, the blocks - it is stylish, fashionable, youth!

Asynchronous unit tests

Let's not stretch the article and omit some details. I think most readers know that the test below will never fail (authorizeWithLogin ... is an asynchronous operation):
')
- (void)testMyAwesomeAPI { [api authorizeWithLogin:kLogin password:kPassword completion:^(NSString *nickname) { STAssertTrue([nickname isEqualToString:@"John"], @""); //code } error:^(NSError *error) { STAssertTrue(false, @""); //code }]; } 

How to make the test wait for the operation to complete?

In fact, making mass. But most of all I liked the idea of ​​a certain 'Marin Todorov'. Its slightly revised class is shown below:

 #import <Foundation/Foundation.h> @interface TestSemaphor : NSObject @property (strong, atomic) NSMutableDictionary* flags; + (TestSemaphor *)sharedInstance; - (BOOL)isLifted:(NSString*)key; - (void)lift:(NSString*)key; - (BOOL)waitForKey:(NSString*)key; - (BOOL)waitForKey:(NSString *)key timeout:(NSTimeInterval)timeout; @end 


 #import "TestSemaphor.h" @implementation TestSemaphor @synthesize flags; +(TestSemaphor *)sharedInstance { static TestSemaphor *sharedInstance = nil; static dispatch_once_t once; dispatch_once(&once, ^{ sharedInstance = [TestSemaphor alloc]; sharedInstance = [sharedInstance init]; }); return sharedInstance; } - (id)init { self = [super init]; if (self != nil) { self.flags = [NSMutableDictionary dictionary]; } return self; } - (BOOL)isLifted:(NSString*)key { return [self.flags objectForKey:key] != nil; } - (void)lift:(NSString*)key { [self.flags setObject:@"YES" forKey:key]; } - (BOOL)waitForKey:(NSString *)key timeout:(NSTimeInterval)timeout { BOOL keepRunning; NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeout]; do { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate]; keepRunning = ![[TestSemaphor sharedInstance] isLifted:key]; if([timeoutDate timeIntervalSinceNow] < 0.0) { [self lift:key]; return NO; } } while (keepRunning); return YES; } - (BOOL)waitForKey:(NSString*)key { return [self waitForKey:key timeout:10.0]; } @end 

We will be interested in the methods lift: and waitForKey:. Let's go straight to the example:

 NSString *key = [NSString UUID]; [api authorizeWithLogin:kLogin password:kPassword completion:^(NSString *nickname) { STAssertTrue([nickname isEqualToString:@"John"], @""); [[TestSemaphor sharedInstance] lift:key]; //code } error:^(NSError *error) { STAssertTrue(false, @""); //code }]; STAssertTrue([[TestSemaphor sharedInstance] waitForKey:key], @"Failed due timeout"); 

The testMyAwesomeAPI method will not transfer control higher until the completion block is called or the wait time is exceeded.
UUID - a unique identifier, the 'key' for this test.

But, as I said, this test has a very annoying problem - it will not be executed if the Internet is not available or the server with the API has fallen.

Server independent unit tests

In order to abandon the server, its response must be replaced. There are many solutions to this problem, but perhaps the most elegant one I've ever met is OHHTTPStubs . According to tradition, an example at once (in my opinion, something more convenient is simply impossible to invent):

 - (void)testMyAwesomeAPI { [OHHTTPStubs addRequestHandler:^OHHTTPStubsResponse*(NSURLRequest *request, BOOL onlyCheck) { return [OHHTTPStubsResponse responseWithFile:@"login.json" contentType:@"text/json" responseTime:0.0]; }]; NSString *key = [NSString UUID]; [api authorizeWithLogin:kLogin password:kPassword completion:^(NSString *nickname) { STAssertTrue([nickname isEqualToString:@"John"], @""); [[TestSemaphor sharedInstance] lift:key]; //code } error:^(NSError *error) { STAssertTrue(false, @""); //code }]; STAssertTrue([[TestSemaphor sharedInstance] waitForKey:key], @"Failed due timeout"); } 

Everything! The next request to the network will be replaced and in response we will receive the contents of the login.json file.
In fact, OHHTTPSpubs is not as simple as it seems, and allows you to configure your behavior quite flexibly, but you can read about it in the project wiki. The only thing worth mentioning explicitly: OHHTTPStubs uses the Private API, make sure that the production code does not use the library .

That's all. Thanks for attention!

Source: https://habr.com/ru/post/164073/


All Articles