Как вы тестируете асинхронный метод? - PullRequest
17 голосов
/ 04 мая 2009

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

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

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

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

Ответы [ 4 ]

30 голосов
/ 24 июня 2012

На ум приходят три способа: 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];
}

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

В этом тесте я просто использую 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);, мы застряли бы до получения сигнала от задачи, которая продолжает работать в фоновой очереди.

* Группа 1040 * Теперь представьте, что вам нужно подождать несколько блоков. Вы можете использовать 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, ^{/*...*/});

1 голос
/ 17 декабря 2013

@ jano Спасибо, что сделал из этой маленькой утилиты из твоего поста

In 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];

}
1 голос
/ 18 мая 2009

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

0 голосов
/ 04 мая 2009

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...