Simple server request mocking + unit-testing block callbacks in Objective-C

  • Tutorial
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 their domain area. And let's say if a server with an API breaks down, then back-end unit tests should detect this, but not the failed tests of my iOS application.

2. Why use blocks, why not target-action, delegation, and so on?
This is a personal preference for 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, blocks are stylish, fashionable, youth!

Asynchronous Unit Tests

We will 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, there are a lot of solutions. But, most of all I liked the idea of ​​a certain 'Marin Todorov'. Its slightly revised class is shown below:

#import 
@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 lift: and waitForKey: methods . 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 above until completion block is called or timeout is exceeded.
UUID - unique identifier, 'key' for this test.

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

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 of those that I have ever met is OHHTTPStubs . According to tradition, just an example (in my opinion, something more convenient is simply impossible to come up with):

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

All! The next request to the network will be substituted and in response we will get the contents of the login.json file.
In fact, OHHTTPStubs is not as simple as it seems, and it allows you to configure your behavior quite flexibly, but you can read more about this on 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!

Also popular now: