Зачем
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/
Добавить комментарий