objective c - tipos - Patrón para la prueba unitaria de la cola asíncrona que llama a la cola principal al finalizar
tipos de pruebas unitarias (6)
En función de varias de las otras respuestas a esta pregunta, configuré esto para mayor comodidad (y diversión): https://github.com/kallewoof/UTAsync
Espero que ayude a alguien.
Esto está relacionado con mi question anterior, pero lo suficientemente diferente como para pensar que lo lanzaría a uno nuevo. Tengo un código que se ejecuta de forma asincrónica en una cola personalizada, luego ejecuta un bloque de finalización en el hilo principal cuando se completa. Me gustaría escribir una prueba unitaria sobre este método. Mi método en MyObject
ve así.
+ (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();
});
}
});
}
Lancé la prueba de cola principal para mayor seguridad, pero siempre llega al dispatch_async
. Mi prueba de unidad se parece a lo siguiente.
- (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");
}
Creé un semáforo para evitar que la prueba termine antes de que el código asíncrono lo haga. Esto funcionaría muy bien si no requiero que el bloque de finalización se ejecute en el hilo principal. Sin embargo, como señalaron algunas personas en la pregunta a la que me refería anteriormente, el hecho de que la prueba se ejecute en el hilo principal y luego encola el bloque de finalización en el hilo principal significa que me quedaré para siempre.
Llamar a la cola principal desde una cola asíncrona es un patrón que veo mucho para actualizar la interfaz de usuario y demás. ¿Alguien tiene un mejor patrón para probar el código asíncrono que llama a la cola principal?
Hay dos formas de ejecutar los bloques enviados a la cola principal para que se ejecuten. El primero es a través de dispatch_main
, como lo mencionan Drewsmits. Sin embargo, como también señaló, hay un gran problema con el uso de dispatch_main
en su prueba: nunca regresa . Simplemente se sentará allí esperando para ejecutar cualquier bloqueo que se presente por el resto de la eternidad. Eso no es tan útil para una prueba unitaria, como te puedes imaginar.
Afortunadamente, hay otra opción. En la sección de COMPATIBILITY
de la página man dispatch_main
, dice esto:
Las aplicaciones de cacao no necesitan llamar a dispatch_main (). Los bloques enviados a la cola principal se ejecutarán como parte de los "modos comunes" de la aplicación principal NSRunLoop o CFRunLoop.
En otras palabras, si está en una aplicación Cocoa, la cola de envío se vacía mediante NSRunLoop
del subproceso principal. Entonces, todo lo que tenemos que hacer es mantener el ciclo de ejecución funcionando mientras esperamos que la prueba termine. Se parece a esto:
- (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");
}
}
La forma más sencilla de ejecutar bloques en la cola principal es llamar a dispatch_main()
desde el hilo principal. Sin embargo, por lo que puedo ver en los documentos, eso nunca volverá, por lo que nunca podrás saber si tu prueba ha fallado.
Otro enfoque es hacer que su prueba de unidad entre en su ciclo de ejecución después del despacho. Luego, el bloque de finalización tendrá la oportunidad de ejecutarse y también tendrá la oportunidad de que el bucle de ejecución expire, luego de lo cual puede considerar que la prueba falló si el bloque de finalización no se ejecutó.
La solución de BJ Homer es la mejor solución hasta ahora. Creé algunas macros basadas en esa solución.
Vea el proyecto aquí 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 evaluará la entrada hasta que la expresión no sea verdadera o se alcance el límite de tiempo. Creo que es difícil hacerlo más limpio que esto
Square incluyó una inteligente adición a SenTestCase en su proyecto SocketRocket que lo hace fácil. Puedes llamarlo así:
[self runCurrentRunLoopUntilTestPasses:^BOOL{
return [someOperation isDone];
} timeout: 60 * 60];
El código está disponible aquí:
Un método alternativo, usando semáforos y batido runloop. Tenga en cuenta que dispatch_semaphore_wait devuelve un valor distinto de cero si se agota el tiempo de espera.
- (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);
}