Шаблон для модульного тестирования асинхронной очереди, которая вызывает основную очередь по завершении - PullRequest
32 голосов
/ 19 октября 2011

Это связано с моим предыдущим вопросом , но достаточно разным, и я решил, что добавлю его в новый. У меня есть некоторый код, который выполняет асинхронный вызов в пользовательской очереди, а затем выполняет блок завершения в главном потоке после завершения. Я хотел бы написать модульный тест вокруг этого метода. Мой метод на 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 ]

58 голосов
/ 20 октября 2011

Существует два способа отправки блоков в основную очередь для запуска.Первый - через 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");
    }
}
18 голосов
/ 14 декабря 2011

Альтернативный метод, использующий семафоры и взбалтывание 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);
}
7 голосов
/ 26 июля 2013

Решение 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) будет оценивать ввод, пока выражение не станет истинным или пока не будет достигнут предел времени.Я думаю, что трудно получить его чище, чем этот

7 голосов
/ 03 декабря 2012

Square включил в свой проект SocketRocket умное дополнение к SenTestCase, которое делает это легко Вы можете назвать это так:

[self runCurrentRunLoopUntilTestPasses:^BOOL{
    return [someOperation isDone];
} timeout: 60 * 60];

Код доступен здесь:

SenTestCase + SRTAdditions.h

SenTestCase + SRTAdditions.m

3 голосов
/ 19 октября 2011

Самый простой способ выполнить блоки в основной очереди - вызвать dispatch_main() из основного потока.Однако, насколько я вижу из документов, это никогда не вернется, поэтому вы никогда не сможете определить, провалился ли ваш тест.

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

2 голосов
/ 30 июня 2013

Основываясь на нескольких других ответах на этот вопрос, я настроил это для удобства (и веселья): https://github.com/kallewoof/UTAsync

Надеюсь, это кому-нибудь поможет.

...