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

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

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

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


person Drewsmits    schedule 19.10.2011    source источник
comment
Я собрал несколько ресурсов по асинхронному тестированию, включая ответ Б.Дж. Гомера, в сообщении в блоге. drewsmitscode.posterous.com/   -  person Drewsmits    schedule 03.05.2012


Ответы (6)


Есть два способа отправить блоки в основную очередь для выполнения. Первый - через dispatch_main, как упоминал Дрюсмитс. Однако, как он также отметил, есть большая проблема с использованием 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");
    }
}
person BJ Homer    schedule 20.10.2011
comment
Еще раз спасибо, что нашли время ответить! Похоже, это неплохой образец для асинхронного тестирования OCUnit. - person Drewsmits; 20.10.2011
comment
среди моих поисковых запросов в Google это было, безусловно, лучшее и самое простое в реализации решение для асинхронного тестирования, которое я нашел. - person thgc; 11.05.2012
comment
Вы должны проверить, что hasCalledBack == NO, прежде чем потерпеть неудачу, верно? Или цикл while () должен каким-то образом возвращаться, если он работает. - person NKijak; 03.08.2012
comment
Да, ты бы хотел это изменить. Я просто скопировал это прямо из сообщения OP. - person BJ Homer; 03.08.2012
comment
Большое спасибо! Так просто! - person Misha Karpenko; 13.09.2012
comment
Я работал с другим разработчиком, пытаясь понять, как это сделать, и полностью забыл о прямом вызове цикла выполнения. Отличный ответ. - person stuckj; 13.11.2013
comment
Этот код предполагает, что я могу передать блок завершения тестируемому коду. Я не уверен, что вообще считаю это жизнеспособным подходом. - person p0lAris; 23.05.2015
comment
Историческое примечание: SenTest был заменен на XCTest, который теперь включает поддержку асинхронных тестов напрямую через XCTestExpectation API. - person BJ Homer; 03.11.2016

Альтернативный метод, использующий семафоры и перемешивание циклов выполнения. Обратите внимание, что 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);
}
person qwzybug    schedule 13.12.2011
comment
Ваше добавление nsrunloop было весьма полезно! - person shawnwall; 27.03.2012

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

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

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

SenTestCase + SRTAdditions.h

SenTestCase + SRTAdditions.m

person dstnbrkr    schedule 02.12.2012

Решение Б.Дж. Гомера - лучшее решение на данный момент. Я создал несколько макросов, основанных на этом решении.

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

person hfossli    schedule 26.07.2013

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

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

person JeremyP    schedule 19.10.2011

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

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

person Kalle    schedule 29.06.2013