iphone - test - ¿Cómo probar las API asíncronas de manera unitaria?
testing aplicaciones moviles (13)
Acabo de escribir una entrada de blog sobre esto (de hecho, comencé un blog porque pensé que este era un tema interesante). Terminé usando el método swizzling para poder llamar al controlador de finalización usando cualquier argumento que quiera sin esperar, lo que me pareció bueno para las pruebas unitarias. Algo como esto:
- (void)swizzledGeocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler
{
completionHandler(nil, nil); //You can test various arguments for the handler here.
}
- (void)testGeocodeFlagsComplete
{
//Swizzle the geocodeAddressString with our own method.
Method originalMethod = class_getInstanceMethod([CLGeocoder class], @selector(geocodeAddressString:completionHandler:));
Method swizzleMethod = class_getInstanceMethod([self class], @selector(swizzledGeocodeAddressString:completionHandler:));
method_exchangeImplementations(originalMethod, swizzleMethod);
MyGeocoder * myGeocoder = [[MyGeocoder alloc] init];
[myGeocoder geocodeAddress]; //the completion handler is called synchronously in here.
//Deswizzle the methods!
method_exchangeImplementations(swizzleMethod, originalMethod);
STAssertTrue(myGeocoder.geocoded, @"Should flag as geocoded when complete.");//You can test the completion handler code here.
}
entrada de blog para cualquier persona que se preocupe.
He instalado Google Toolbox para Mac en Xcode y seguí las instrucciones para configurar las pruebas unitarias que se encuentran here .
Todo funciona muy bien, y puedo probar mis métodos sincrónicos en todos mis objetos absolutamente bien. Sin embargo, la mayoría de las API complejas que realmente deseo probar devuelven resultados de manera asincrónica llamando a un método en un delegado; por ejemplo, una llamada a un sistema de descarga y actualización de archivos regresará inmediatamente y luego ejecutará un método -fileDownloadDidComplete: cuando el archivo termine la descarga .
¿Cómo probaría esto como una prueba unitaria?
Parece que me gustaría que la función testDownload, o al menos el framework de prueba, ''espere'' a que se ejecute el método fileDownloadDidComplete :.
EDITAR: Ahora he cambiado al uso del sistema XCTest incorporado de XCode y he descubierto que TVRSMonitor en Github ofrece una forma totalmente sencilla de usar semáforos para esperar a que se completen las operaciones de sincronización.
Por ejemplo:
- (void)testLogin {
TRVSMonitor *monitor = [TRVSMonitor monitor];
__block NSString *theToken;
[[Server instance] loginWithUsername:@"foo" password:@"bar"
success:^(NSString *token) {
theToken = token;
[monitor signal];
}
failure:^(NSError *error) {
[monitor signal];
}];
[monitor wait];
XCTAssert(theToken, @"Getting token");
}
Aprecio que esta pregunta fue hecha y respondida hace casi un año, pero no puedo evitar estar en desacuerdo con las respuestas dadas. Probar las operaciones asíncronas, particularmente las operaciones de red, es un requisito muy común, y es importante acertar. En el ejemplo dado, si depende de las respuestas reales de la red, pierde parte del valor importante de sus pruebas. Específicamente, sus pruebas dependen de la disponibilidad y la corrección funcional del servidor con el que se está comunicando; esta dependencia hace tus pruebas
- más frágil (¿qué pasa si el servidor se cae?)
- menos completo (¿cómo se prueba constantemente una respuesta de falla o error de red?)
- significativamente más lento imagine probar esto:
Las pruebas unitarias deben ejecutarse en fracciones de segundo. Si tiene que esperar una respuesta de red de varios segundos cada vez que ejecuta las pruebas, es menos probable que las ejecute con frecuencia.
La prueba unitaria se trata principalmente de encapsular dependencias; desde el punto de vista de su código bajo prueba, suceden dos cosas:
- Su método inicia una solicitud de red, probablemente al crear una instancia de NSURLConnection.
- El delegado que especificó recibe una respuesta a través de ciertas llamadas a métodos.
A su delegado no le importa, o no debería, el origen de la respuesta, ya sea de una respuesta real de un servidor remoto o de su código de prueba. Puede aprovechar esto para probar operaciones asincrónicas simplemente generando las respuestas usted mismo. Sus pruebas se ejecutarán mucho más rápido, y puede probar con fiabilidad las respuestas de éxito o fracaso.
Esto no quiere decir que no deba ejecutar pruebas contra el servicio web real con el que está trabajando, pero esas son pruebas de integración y pertenecen a su propio conjunto de pruebas. Las fallas en ese conjunto pueden significar que el servicio web tiene cambios o simplemente está inactivo. Como son más frágiles, su automatización tiende a tener menos valor que la automatización de las pruebas unitarias.
En cuanto a cómo proceder exactamente para probar las respuestas asíncronas a una solicitud de red, tiene un par de opciones. Simplemente podría probar el delegado de forma aislada llamando directamente a los métodos (p. Ej., [SomeDelegate connection: connection didReceiveResponse: someResponse]). Esto funcionará un poco, pero es un poco incorrecto. El delegado que proporciona su objeto puede ser solo uno de los múltiples objetos en la cadena de delegados para un objeto NSURLConnection específico; si llama directamente a los métodos de su delegado, es posible que le falte alguna pieza clave de funcionalidad proporcionada por otro delegado que esté más adelante en la cadena. Como una mejor alternativa, puede resguardar el objeto NSURLConnection que crea y que envíe los mensajes de respuesta a toda su cadena de delegados. Hay bibliotecas que reabrirán NSURLConnection (entre otras clases) y lo harán por usted. Por ejemplo: https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m
Encontré este artículo en este que es un muc http://dadabeatnik.wordpress.com/2013/09/12/xcode-and-asynchronous-unit-testing/
Escribí un pequeño ayudante que hace que sea fácil probar API asíncrona. Primero el ayudante:
static inline void hxRunInMainLoop(void(^block)(BOOL *done)) {
__block BOOL done = NO;
block(&done);
while (!done) {
[[NSRunLoop mainRunLoop] runUntilDate:
[NSDate dateWithTimeIntervalSinceNow:.1]];
}
}
Puedes usarlo así:
hxRunInMainLoop(^(BOOL *done) {
[MyAsyncThingWithBlock block:^() {
/* Your test conditions */
*done = YES;
}];
});
Solo continuará si se convierte en TRUE
, así que asegúrese de configurarlo una vez completado. Por supuesto, podría agregar un tiempo de espera al ayudante si lo desea,
Esto es complicado. Creo que necesitarás configurar un runloop en tu prueba y también la capacidad de especificar ese runloop en tu código asíncrono. De lo contrario, las devoluciones de llamada no se realizarán ya que se ejecutan en un runloop.
Supongo que podrías ejecutar el runloop por s de corta duración en un bucle. Y permita que la devolución de llamada establezca una variable de estado compartida. O tal vez incluso simplemente pedirle a la devolución de llamada que finalice el runloop. De esa forma tu sabes que la prueba ha terminado. Debería poder verificar los tiempos de espera al detener el ciclo después de un cierto tiempo. Si eso sucede, se produce un tiempo de espera.
Nunca he hecho esto, pero tendré que hacerlo pronto, creo. Por favor comparte tus resultados :-)
Implementé la solución propuesta por Thomas Tempelmann y, en general, funciona bien para mí.
Sin embargo, hay un gotcha. Supongamos que la unidad que se probará contiene el siguiente código:
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:selector withObject:nil afterDelay:1.0];
});
Es posible que nunca se llame al selector, ya que le indicamos al hilo principal que se bloquee hasta que finalice la prueba:
[testBase.lock lockWhenCondition:1];
En general, podríamos deshacernos de NSConditionLock por completo y simplemente usar la clase GHAsyncTestCase lugar.
Así es como lo uso en mi código:
@interface NumericTestTests : GHAsyncTestCase { }
@end
@implementation NumericTestTests {
BOOL passed;
}
- (void)setUp
{
passed = NO;
}
- (void)testMe {
[self prepare];
MyTest *test = [MyTest new];
[test run: ^(NSError *error, double value) {
passed = YES;
[self notify:kGHUnitWaitStatusSuccess];
}];
[test runTest:fakeTest];
[self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0];
GHAssertTrue(passed, @"Completion handler not called");
}
Mucho más limpio y no bloquea el hilo principal.
Me encontré con la misma pregunta y encontré una solución diferente que me funciona.
Utilizo el enfoque de la "vieja escuela" para convertir las operaciones asincrónicas en un flujo de sincronización mediante el uso de un semáforo de la siguiente manera:
// create the object that will perform an async operation
MyConnection *conn = [MyConnection new];
STAssertNotNil (conn, @"MyConnection init failed");
// create the semaphore and lock it once before we start
// the async operation
NSConditionLock *tl = [NSConditionLock new];
self.theLock = tl;
[tl release];
// start the async operation
self.testState = 0;
[conn doItAsyncWithDelegate:self];
// now lock the semaphore - which will block this thread until
// [self.theLock unlockWithCondition:1] gets invoked
[self.theLock lockWhenCondition:1];
// make sure the async callback did in fact happen by
// checking whether it modified a variable
STAssertTrue (self.testState != 0, @"delegate did not get called");
// we''re done
[self.theLock release]; self.theLock = nil;
[conn release];
Asegúrate de invocar
[self.theLock unlockWithCondition:1];
En el (los) delegado (s) entonces
Me parece muy conveniente usar https://github.com/premosystems/XCAsyncTestCase
Agrega tres métodos muy útiles para XCTestCase
@interface XCTestCase (AsyncTesting)
- (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout;
- (void)waitForTimeout:(NSTimeInterval)timeout;
- (void)notify:(XCTAsyncTestCaseStatus)status;
@end
que permiten pruebas muy limpias. Un ejemplo del proyecto en sí:
- (void)testAsyncWithDelegate
{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
[NSURLConnection connectionWithRequest:request delegate:self];
[self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:10.0];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"Request Finished!");
[self notify:XCTAsyncTestCaseStatusSucceeded];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"Request failed with error: %@", error);
[self notify:XCTAsyncTestCaseStatusFailed];
}
Mi respuesta es que las pruebas unitarias, conceptualmente, no son adecuadas para probar las operaciones de asincronización. Una operación de asincronización, como una solicitud al servidor y el manejo de la respuesta, no ocurre en una unidad, sino en dos unidades.
Para relacionar la respuesta a la solicitud, debe de alguna manera bloquear la ejecución entre las dos unidades o mantener los datos globales. Si bloquea la ejecución, entonces su programa no se está ejecutando normalmente, y si mantiene datos globales, habrá agregado funciones extrañas que pueden contener errores. Cualquiera de las soluciones viola toda la idea de las pruebas unitarias y requiere que inserte un código de prueba especial en su aplicación; y luego de la prueba de su unidad, aún tendrá que apagar su código de prueba y realizar pruebas "manuales" pasadas de moda. El tiempo y el esfuerzo invertidos en las pruebas unitarias se desperdician al menos en parte.
Para profundizar en la solución de @ St3fan, puede probar esto luego de iniciar la solicitud:
- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs
{
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if ([timeoutDate timeIntervalSinceNow] < 0.0)
{
break;
}
}
while (!done);
return done;
}
De otra manera:
//block the thread in 0.1 second increment, until one of callbacks is received.
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
//setup timeout
float waitIncrement = 0.1f;
int timeoutCounter = (int)(30 / waitIncrement); //30 sec timeout
BOOL controlConditionReached = NO;
// Begin a run loop terminated when the downloadComplete it set to true
while (controlConditionReached == NO)
{
[theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:waitIncrement]];
//control condition is set in one of your async operation delegate methods or blocks
controlConditionReached = self.downloadComplete || self.downloadFailed ;
//if there''s no response - timeout after some time
if(--timeoutCounter <= 0)
{
break;
}
}
Si está utilizando una biblioteca como AFNetworking o ASIHTTPRequest y tiene sus solicitudes administradas a través de una NSOperation (o subclase con esas bibliotecas), entonces es fácil probarlas contra un servidor de prueba / desarrollo con NSOperationQueue:
En prueba:
// create request operation
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:request];
[queue waitUntilAllOperationsAreFinished];
// verify response
Esto esencialmente ejecuta un runloop hasta que la operación se haya completado, lo que permite que todas las devoluciones de llamada se produzcan en los hilos de fondo como lo harían normalmente.
St3fan, eres un genio. ¡Muchas gracias!
Así es como lo hice usando tu sugerencia.
''Downloader'' define un protocolo con un método DownloadDidComplete que se dispara al finalizar. Hay una variable miembro de BOOL ''downloadComplete'' que se utiliza para terminar el ciclo de ejecución.
-(void) testDownloader {
downloadComplete = NO;
Downloader* downloader = [[Downloader alloc] init] delegate:self];
// ... irrelevant downloader setup code removed ...
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
// Begin a run loop terminated when the downloadComplete it set to true
while (!downloadComplete && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
}
-(void) DownloaderDidComplete:(Downloader*) downloader withErrors:(int) errors {
downloadComplete = YES;
STAssertNotEquals(errors, 0, @"There were errors downloading!");
}
El ciclo de ejecución podría ejecutarse para siempre, por supuesto. ¡Lo mejoraré más tarde!