Зачем
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, убедитесь, что продакшен код не использует библиотеку.
Вот и все. Спасибо за внимание!