Подтвердить что ты не робот

Как вы тестируете асинхронный метод?

У меня есть объект, который извлекает XML или JSON по сети. Как только эта выборка завершена, она вызывает селектор, передавая возвращаемые данные. Так, например, у меня было бы что-то вроде:

-(void)testResponseWas200
{
    [MyObject get:@"foo.xml" withTarget:self selector:@selector(dataFinishedLoading:)];  
}

Я попробовал маршрут реализации dataFinishedLoading в классе Test и попытался протестировать внутри этого метода, но набор тестов просто блокируется. Это похоже на случай насмешек, но мне интересно, столкнулись ли с этим другие и как они справились с этим.

FYI: Я использую gh-unit для тестирования, и любой метод с префиксом test * выполняется автоматически.

4b9b3361

Ответ 1

Три способа, которые приходят на ум, - это: NSRunLoop, семафоры и группы.

NSRunLoop

__block bool finished = false;

// For testing purposes we create this asynchronous task 
// that starts after 3 seconds and takes 1 second to execute.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);
dispatch_time_t threeSeconds = dispatch_time(DISPATCH_TIME_NOW, 3LL * NSEC_PER_SEC);
dispatch_after(threeSeconds, queue, ^{ 
    sleep(1); // replace this with your task
    finished = true; 
});

// loop until the flag is set from inside the task
while (!finished) {
    // spend 1 second processing events on each loop
    NSDate *oneSecond = [NSDate dateWithTimeIntervalSinceNow:1];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:oneSecond];
}

A NSRunLoop - это цикл, который обрабатывает события, такие как сетевые порты, клавиатуру или любой другой источник входного сигнала, который вы подключаете, и возвращает после обработки этих событий или по истечении срока. Когда нет событий для обработки, цикл запуска помещает поток в режим сна. Все приложения Cocoa и Core Foundation имеют цикл запуска под ним. Вы можете узнать больше о циклах запуска в Руководстве по программированию Apple Threading: Run Loops или в Mike Ash Friday Q & A 2010- 01-01: Внутренние NSRunLoop.

В этом тесте я просто использую NSRunLoop, чтобы спать поток на секунду. Без него постоянный цикл в while будет потреблять 100% ядра процессора.

Если блок и логический флаг создаются в одной и той же лексической области (например, как внутри метода), тогда флага требуется __block классификатор хранилища быть изменчивым. Если бы флаг был глобальной переменной, он бы не нуждался в нем.

Если тест завершится с ошибкой перед установкой флага, поток застрял в ожидании навсегда. Добавьте ограничение по времени, чтобы избежать этого:

NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:2];
while (!finished && [timeout timeIntervalSinceNow]>0) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                             beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
if (!finished) NSLog(@"test failed with timeout");

Если вы используете этот код для модульного тестирования, альтернативный способ вставки тайм-аута - отправить блок с утверждением:

// taken from https://github.com/JaviSoto/JSBarrierOperationQueue/blob/master/JSBarrierOperationQueueTests/JSBarrierOperationQueueTests.m#L118
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2LL * NSEC_PER_SEC);
dispatch_after(timeout, dispatch_get_main_queue(), ^(void){
    STAssertTrue(done, @"Should have finished by now");
});

Семафор

Подобная идея, но спящий, пока семафор не изменится, или до определенного срока:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

// signal the semaphore after 3 seconds using a global queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3LL*NSEC_PER_SEC), queue, ^{ 
    sleep(1);
    dispatch_semaphore_signal(semaphore);
});

// wait with a time limit of 5 seconds
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5LL*NSEC_PER_SEC);
if (dispatch_semaphore_wait(semaphore, timeout)==0) {
    NSLog(@"success, semaphore signaled in time");
} else {
    NSLog(@"failure, semaphore didn't signal in time");
}

dispatch_release(semaphore);

Если вместо этого мы ждали навсегда dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);, мы бы застряли, пока не получим сигнал от задачи, которая продолжает работать в фоновом режиме.

Группа

Теперь представьте, что вам нужно подождать несколько блоков. Вы можете использовать флаг int как флаг или создать семафор, который начинается с большего числа, или вы можете сгруппировать блоки и дождаться окончания группы. В этом примере я делаю это позже, используя только один блок:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);

// dispatch work to the given group and queue
dispatch_group_async(group,queue,^{
    sleep(1); // replace this with your task
});

// wait two seconds for the group to finish
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2LL*NSEC_PER_SEC);
if (dispatch_group_wait(group, timeout)==0) {
    NSLog(@"success, dispatch group completed in time");
} else {
    NSLog(@"failure, dispatch group did not complete in time");
}

dispatch_release(group);

Если по какой-то причине (для очистки ресурсов?) вы хотите запустить блок после завершения группы, используйте dispatch_group_notify(group,queue, ^{/*...*/});

Ответ 2

Асинхронные обратные вызовы часто требуют запуска цикла сообщений. Частным шаблоном является остановка цикла сообщения после вызова обратного вызова в тестовом коде. В противном случае цикл просто ждет следующих задач, и их не будет.

Ответ 3

@jano Спасибо, я сделал из этого небольшого использования из вашего сообщения.

В PYTestsUtils.m

+ (void)waitForBOOL:(BOOL*)finished forSeconds:(int)seconds {
    NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:seconds];
    while (!*finished && [timeout timeIntervalSinceNow]>0) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
    }
}

в тестовом файле

- (void)testSynchronizeTime
{
    __block BOOL finished = NO;
    [self.connection synchronizeTimeWithSuccessHandler:^(NSTimeInterval serverTime) {
        NSLog(@"ServerTime %f", serverTime);
        finished = YES;
    } errorHandler:^(NSError *error) {
        STFail(@"Cannot get ServerTime %@", error);
        finished = YES;
    }];

    [PYTestsUtils waitForBOOL:&finished forSeconds:10];
    if (! finished)
        STFail(@"Cannot get ServerTime within 10 seconds");

}

Изменение

добавить в PYTestsUtils.m

+ (void)execute:(PYTestExecutionBlock)block ifNotTrue:(BOOL*)finished afterSeconds:(int)seconds {
    [self waitForBOOL:finished forSeconds:seconds];
    if (! *finished) block();
}

использование:

- (void)testSynchronizeTime
{
    __block BOOL finished = NO;
    [self.connection synchronizeTimeWithSuccessHandler:^(NSTimeInterval serverTime) {
        NSLog(@"ServerTime %f", serverTime);
        finished = YES;
    } errorHandler:^(NSError *error) {
        STFail(@"Cannot get ServerTime %@", error);
        finished = YES;
    }];

    [PYTestsUtils execute:^{
        STFail(@"Cannot get ServerTime within 10 seconds");
    } ifNotTrue:&finished afterSeconds:10];

}

Ответ 4

Одним из лучших способов тестирования асинхронного и многопоточного кода является ведение журнала событий. Ваш код должен регистрировать события в интересное или полезное время. Часто одному событию достаточно информации, чтобы доказать, что логика работает правильно. В некоторых случаях событиям понадобятся полезная информация или другая метаинформация, чтобы они могли быть сопряжены или скованы.

Это наиболее полезно, когда время выполнения или операционная система поддерживают эффективный и надежный механизм событий. Это позволяет вашему продукту поставляться с событиями в "розничной" версии. В этом случае ваши события активируются только тогда, когда вам нужно отлаживать проблему или запускать unit test, чтобы доказать, что тонны работают правильно.

Наличие событий в розничном (производственном) коде позволяет тестировать и отлаживать на любой платформе. Это огромное преимущество по сравнению с отладочным или проверенным кодом.

Обратите внимание, как утверждают, будьте осторожны, когда вы ставите события - они могут быть дорогими, если вы часто заходите в систему. Но хорошей новостью является то, что современные ОС и некоторые платформы приложений поддерживают механизмы событий, которые легко поддерживают 10 тысяч событий. Некоторая поддержка, использующая трассировку стека для выбранных событий. Это может быть очень мощным, но обычно требуется, чтобы символы были доступны в какой-то момент времени - либо при протоколировании, либо в процессе последующей обработки трассировки в целевой системе.