Шаблон для модульного тестирования асинхронной очереди, которая вызывает основную очередь по завершении
Это связано с моим предыдущим вопросом, но достаточно разным, и я решил добавить его в новый. У меня есть некоторый код, который выполняет асинхронную в пользовательской очереди, а затем выполняет блок завершения в главном потоке, когда завершено. Я хотел бы написать модульный тест вокруг этого метода. Мой метод на MyObject
выглядит так
+ (void)doSomethingAsyncThenRunCompletionBlockOnMainQueue:(void (^)())completionBlock {
dispatch_queue_t customQueue = dispatch_queue_create("com.myObject.myCustomQueue", 0);
dispatch_async(customQueue, ^(void) {
dispatch_queue_t currentQueue = dispatch_get_current_queue();
dispatch_queue_t mainQueue = dispatch_get_main_queue();
if (currentQueue == mainQueue) {
NSLog(@"already on main thread");
completionBlock();
} else {
dispatch_async(mainQueue, ^(void) {
NSLog(@"NOT already on main thread");
completionBlock();
});
}
});
}
Я бросил в основной тест очереди для дополнительной безопасности, но он всегда поражает dispatch_async
, Мой юнит-тест выглядит следующим образом.
- (void)testDoSomething {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
void (^completionBlock)(void) = ^(void){
NSLog(@"Completion Block!");
dispatch_semaphore_signal(sema);
};
[MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];
// Wait for async code to finish
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);
STFail(@"I know this will fail, thanks");
}
Я создаю семафор, чтобы заблокировать тестирование до завершения асинхронного кода. Это будет прекрасно работать, если мне не потребуется запускать блок завершения в главном потоке. Однако, как отметили несколько человек в ответе на вопрос, который я упомянул выше, тот факт, что тест выполняется в главном потоке, а затем я ставлю в очередь блок завершения в главном потоке, означает, что я просто зависну навсегда.
Вызов основной очереди из асинхронной очереди - это шаблон, который я часто вижу для обновления пользовательского интерфейса и тому подобное. У кого-нибудь есть лучший шаблон для тестирования асинхронного кода, который перезванивает в главную очередь?
6 ответов
Существует два способа отправки блоков в основную очередь для запуска. Первый через dispatch_main
, как упоминалось Drewsmits. Однако, как он также отметил, есть большая проблема с использованием dispatch_main
в вашем тесте: он никогда не возвращается. Он просто будет сидеть и ждать, пока не пройдут любые блоки, которые встретятся ему на всю оставшуюся вечность. Это не так полезно для модульного теста, как вы можете себе представить.
К счастью, есть еще один вариант. в COMPATIBILITY
раздел dispatch_main
страница руководства, это говорит это:
Приложения какао не должны вызывать dispatch_main(). Блоки, отправленные в основную очередь, будут выполняться как часть "общих режимов" основного NSRunLoop или CFRunLoop приложения.
Другими словами, если вы находитесь в приложении Какао, очередь отправки истощается основным потоком NSRunLoop
, Таким образом, все, что нам нужно сделать, это запустить цикл выполнения, пока мы ожидаем завершения теста. Это выглядит так:
- (void)testDoSomething {
__block BOOL hasCalledBack = NO;
void (^completionBlock)(void) = ^(void){
NSLog(@"Completion Block!");
hasCalledBack = YES;
};
[MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];
// Repeatedly process events in the run loop until we see the callback run.
// This code will wait for up to 10 seconds for something to come through
// on the main queue before it times out. If your tests need longer than
// that, bump up the time limit. Giving it a timeout like this means your
// tests won't hang indefinitely.
// -[NSRunLoop runMode:beforeDate:] always processes exactly one event or
// returns after timing out.
NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:10];
while (hasCalledBack == NO && [loopUntil timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:loopUntil];
}
if (!hasCalledBack)
{
STFail(@"I know this will fail, thanks");
}
}
Альтернативный метод, использующий семафоры и взбалтывание. Обратите внимание, что dispatch_semaphore_wait возвращает ненулевое значение, если время ожидания истекло.
- (void)testFetchSources
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[MyObject doSomethingAsynchronousWhenDone:^(BOOL success) {
STAssertTrue(success, @"Failed to do the thing!");
dispatch_semaphore_signal(semaphore);
}];
while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW))
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
dispatch_release(semaphore);
}
Square включила в свой проект SocketRocket умное дополнение к SenTestCase, которое делает это легко. Вы можете назвать это так:
[self runCurrentRunLoopUntilTestPasses:^BOOL{
return [someOperation isDone];
} timeout: 60 * 60];
Код доступен здесь:
Решение BJ Homer на данный момент является лучшим решением. Я создал несколько макросов, построенных на этом решении.
Проверьте проект здесь https://github.com/hfossli/AGAsyncTestHelper
- (void)testDoSomething {
__block BOOL somethingIsDone = NO;
void (^completionBlock)(void) = ^(void){
NSLog(@"Completion Block!");
somethingIsDone = YES;
};
[MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];
WAIT_WHILE(!somethingIsDone, 1.0);
NSLog(@"This won't be reached until async job is done");
}
WAIT_WHILE(expressionIsTrue, seconds)
-macro будет оценивать ввод, пока выражение не станет истинным или пока не будет достигнут лимит времени. Я думаю, что это сложнее, чем это
Самый простой способ выполнить блоки в главной очереди - вызвать dispatch_main()
из основного потока. Однако, насколько я могу судить по документам, это никогда не вернется, поэтому вы никогда не сможете определить, провалился ли ваш тест.
Другой подход - заставить ваш модульный тест перейти в цикл выполнения после отправки. Тогда у блока завершения будет возможность выполнить, и у вас также будет возможность для таймаута цикла выполнения, после которого вы можете посчитать, что тест не пройден, если блок завершения не был запущен.
Основываясь на нескольких других ответах на этот вопрос, я настроил это для удобства (и веселья): https://github.com/kallewoof/UTAsync
Надеюсь, это поможет кому-то.