Шаблон для очереди асинхронного тестирования, который вызывает основную очередь при завершении
Это связано с моим предыдущим question, но достаточно разным, что я решил, что я брошу его в новый. У меня есть код, который запускает async в пользовательской очереди, а затем завершает блок завершения в основном потоке, когда он завершен. Я хотел бы написать unit test вокруг этого метода. Мой метод в 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
. Мой unit test выглядит следующим образом.
- (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");
}
Я создаю семафор, чтобы блокировать проверку до завершения асинхронного кода. Это было бы здорово, если бы я не требовал выполнения блока завершения для основного потока. Тем не менее, как отмечают пара людей в вопросе, который я связал выше, тот факт, что тест выполняется в основном потоке, а затем я вставляю блок завершения в основной поток, я просто буду вешать навсегда.
Вызов основной очереди из очереди async - это шаблон, который я вижу много для обновления пользовательского интерфейса и т.д. У кого-нибудь есть лучший образец для тестирования асинхронного кода, который обращается к основной очереди?
Ответы
Ответ 1
Существует два способа получить блоки, отправленные в основную очередь для запуска. Первый - через dispatch_main
, как упоминал Дрюсмит. Однако, как он также отметил, существует большая проблема с использованием dispatch_main
в вашем тесте: он никогда не возвращает. Он будет просто сидеть там, ожидая, чтобы запустить любые блоки, которые приходят к нему на всю оставшуюся вечность. Это не так полезно для unit test, как вы можете себе представить.
К счастью, есть еще один вариант. В разделе COMPATIBILITY
справочной страницы dispatch_main
говорится следующее:
Cocoa приложениям не нужно вызывать dispatch_main(). Блоки, представленные основная очередь будет выполняться как часть "общих режимов" основного приложения NSRunLoop или CFRunLoop.
Другими словами, если вы находитесь в приложении Cocoa, очередь отправки сбрасывается основным потоком 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");
}
}
Ответ 2
Альтернативный метод с использованием семафоров и взбивания runloop. Обратите внимание, что 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);
}
Ответ 3
Квадрат включал умное дополнение к SenTestCase в их проекте SocketRocket, что делает это легко. Вы можете назвать это следующим образом:
[self runCurrentRunLoopUntilTestPasses:^BOOL{
return [someOperation isDone];
} timeout: 60 * 60];
Код доступен здесь:
SenTestCase+SRTAdditions.h
SenTestCase + SRTAdditions.m
Ответ 4
Решение BJ Гомера - наилучшее решение. Я создал макрос, построенный на этом решении.
Посмотрите проект здесь 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 будет оценивать ввод до тех пор, пока выражение не будет истинным или не будет достигнут срок. Я думаю, что это трудно сделать чище, чем это.
Ответ 5
Самый простой способ выполнить блоки в главной очереди - вызвать dispatch_main()
из основного потока. Однако, насколько я могу видеть из документов, это никогда не вернется, поэтому вы никогда не сможете сказать, завершился ли тест.
Другой подход заключается в том, чтобы сделать ваш unit test включенным в цикл запуска после отправки. Тогда блок завершения будет иметь возможность выполнить, и у вас также будет возможность выполнить цикл выполнения, после чего вы можете считать, что тест не прошел, если блок завершения не запущен.
Ответ 6
Основываясь на нескольких других ответах на этот вопрос, я настроил это для удобства (и весело): https://github.com/kallewoof/UTAsync
Надеюсь, что это поможет кому-то.