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

в 15:41, , рубрики: blocks, cocoa touch, mac os x, разработка под iOS, метки: , ,
Зачем

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

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

Автор: garnett

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js