Простой мокинг запросов к серверу + unit-тестирование блоковых коллбэков в Objective-C

от автора

Зачем

1. Зачем подменять ответ сервера?
Я всегда был и буду сторонником подхода, когда каждый отвечает за свою доменную область. И скажем, если сервер с API сломался, то обнаружить это должны юнит-тесты бэк-енда, а не свалившиеся тесты моего iOS-приложения.

2. Зачем использовать блоки, почему не target-action, делегирование и так далее?
Это личное предпочтение каждого, почти во всех ситуациях разрабатываемые мной объекты будут иметь блоковые коллбэки а не вызывать методы делегата. Для меня это работает и особых проблем с этим подходом я не испытал. В конце концов, блоки — это стильно, модно, молодежно!

Асинхронные юнит-тесты

Не будем растягивать статью и опустим некоторые детали. Думаю, большинство читателей знают, что тест, приведенный ниже, никогда не упадет (authorizeWithLogin… — асинхронная операция):

- (void)testMyAwesomeAPI {     [api authorizeWithLogin:kLogin password:kPassword completion:^(NSString *nickname) {         STAssertTrue([nickname isEqualToString:@"John"], @"");         //code     } error:^(NSError *error) {         STAssertTrue(false, @"");         //code     }]; }

Как же сделать так, чтобы тест дождался завершения операции?

На самом деле, решений масса. Но, больше всего мне понравилась идея некоего ‘Marin Todorov’. Его слегка переработанный класс приведен ниже:

#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

Нас будут интересовать методы lift: и waitForKey:. Перейдем сразу к примеру:

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

Метод testMyAwesomeAPI не передаст управление выше до тех пор, пока не будет вызван completion блок или будет превышено время ожидания.
UUID — уникальный идентификатор, ‘ключ’ для данного теста.

Но, как я уже говорил, у этого теста есть очень назойливая проблема — он не будет выполнен, если отсутсвует интернет или сервер с API упал.

Юнит-тесты, не зависящие от сервера

Для того, чтобы отказаться от сервера, его ответ необходимо подменить. Существует много решений данной проблемы, но, пожалуй, наиболее изящное из тех, которые я когда-либо встречал, это OHHTTPStubs. По традиции, сразу пример (на мой взгляд, что-то более удобное просто невозможно придумать):

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

Все! Следующий запрос к сети будет подменен и в ответ мы получим содержимое файла login.json.
На самом деле, OHHTTPStubs не так прост, как кажется, и позволяет достаточно гибко конфигурировать свое поведение, но об этом можно почитать в вики проекта. Единственное, что стоит укзать явно: OHHTTPStubs использует Private API, убедитесь, что продакшен код не использует библиотеку.

Вот и все. Спасибо за внимание!

ссылка на оригинал статьи http://habrahabr.ru/post/164073/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *