ios objective-c nsurlsession xctest nsurlsessiontask

¿Cómo hago una prueba unitaria de la solicitud y respuesta HTTP utilizando NSURLSession en iOS 7.1?



objective-c xctest (1)

Me gustaría saber cómo "probar por unidad" las solicitudes y respuestas HTTP mediante NSURLSession. En este momento, no se llama a mi código de bloqueo de finalización cuando se ejecuta como una prueba unitaria. Sin embargo, cuando se ejecuta el mismo código desde AppDelegate (didFinishWithLaunchingOptions), se llama al código dentro del bloque de finalización. Como se sugiere en este hilo, no se llama al controlador de finalización NSURLSessionDataTask dataTaskWithURL , se necesita el uso de semáforos y / o dispatch_group "para asegurarse de que el hilo principal esté bloqueado hasta que finalice la solicitud de red".

Mi código postal HTTP se parece a lo siguiente.

@interface LoginPost : NSObject - (void) post; @end @implementation LoginPost - (void) post { NSURLSessionConfiguration* conf = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession* session = [NSURLSession sessionWithConfiguration:conf delegate:nil delegateQueue:[NSOperationQueue mainQueue]]; NSURL* url = [NSURL URLWithString:@"http://www.example.com/login"]; NSString* params = @"[email protected]&password=test"; NSMutableRequest* request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; [request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]]; NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSLog(@"Response:%@ %@/n", response, error); //code here never gets called in unit tests, break point here never is triggered as well //response is actually deserialized to custom object, code omitted }]; [task resume]; } @end

El método que prueba esto se parece a lo siguiente.

- (void) testPost { LoginPost* loginPost = [LoginPost alloc]; [loginPost post]; //XCTest continues by operating assertions on deserialized HTTP response //code omitted }

En mi AppDelegate , el código de bloque de finalización funciona y se parece a lo siguiente.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //generated code LoginPost* loginPost = [LoginPost alloc]; [loginPost post]; }

¿Algún indicador sobre cómo ejecutar el bloque de finalización cuando se ejecuta dentro de una prueba unitaria? Soy un recién llegado a iOS, por lo que un ejemplo claro realmente ayudaría.

  • Nota: Me doy cuenta de que lo que estoy preguntando puede que no sea una prueba "unitaria" en sentido estricto, ya que confío en un servidor HTTP como parte de mi prueba (es decir, lo que estoy preguntando es más como una prueba de integración).
  • Nota: Me doy cuenta de que hay otro subproceso. Unidades de prueba con NSURLSession sobre pruebas de unidad con NSURLSession, pero no quiero simular respuestas.

Xcode 6 ahora maneja pruebas asíncronas con XCTestExpectation . Al probar un proceso asíncrono, se establece la "expectativa" de que este proceso se completará de forma asíncrona. Después de emitir el proceso asincrónico, se espera que la expectativa se cumpla durante un período de tiempo fijo, y cuando la consulta finalice, asincrónicamente cumplir con las expectativas.

Por ejemplo:

- (void)testDataTask { XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"]; NSURL *url = [NSURL URLWithString:@"http://www.apple.com"]; NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { XCTAssertNil(error, @"dataTaskWithURL error %@", error); if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode]; XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode); } XCTAssert(data, @"data nil"); // do additional tests on the contents of the `data` object here, if you want // when all done, Fulfill the expectation [expectation fulfill]; }]; [task resume]; [self waitForExpectationsWithTimeout:10.0 handler:nil]; }

Mi respuesta anterior, a continuación, es anterior a XCTestExpectation , pero la guardaré para propósitos históricos.

Debido a que su prueba se está ejecutando en la cola principal y porque su solicitud se está ejecutando de forma asíncrona, su prueba no capturará los eventos en el bloque de finalización. Debe utilizar un semáforo o un grupo de envío para que la solicitud sea sincrónica.

Por ejemplo:

- (void)testDataTask { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); NSURL *url = [NSURL URLWithString:@"http://www.apple.com"]; NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { XCTAssertNil(error, @"dataTaskWithURL error %@", error); if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode]; XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode); } XCTAssert(data, @"data nil"); // do additional tests on the contents of the `data` object here, if you want // when all done, signal the semaphore dispatch_semaphore_signal(semaphore); }]; [task resume]; long rc = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 60.0 * NSEC_PER_SEC)); XCTAssertEqual(rc, 0, @"network request timed out"); }

El semáforo asegurará que la prueba no se complete hasta que la solicitud lo haga.

Claramente, mi prueba anterior está haciendo una solicitud HTTP aleatoria, pero espero que ilustre la idea. Y mis varias declaraciones XCTAssert identificarán cuatro tipos de errores:

  1. El objeto NSError no era nulo.

  2. El código de estado HTTP no era 200.

  3. El objeto NSData era nulo.

  4. El bloque de finalización no se completó en 60 segundos.

Presumiblemente, también agregaría pruebas para el contenido de la respuesta (lo que no hice en este ejemplo simplificado).

Tenga en cuenta que la prueba anterior funciona porque no hay nada en mi bloque de finalización que esté enviando nada a la cola principal. Si está probando esto con una operación asíncrona que requiere la cola principal (lo que ocurrirá si no es cuidadoso al usar AFNetworking o enviar manualmente a la cola principal), puede obtener puntos muertos con el patrón anterior (porque estamos bloqueando) el hilo principal a la espera de que finalice la solicitud de red). Pero en el caso de NSURLSession , este patrón funciona muy bien.

Preguntó sobre hacer pruebas desde la línea de comandos, independientemente del simulador. Hay un par de aspectos:

  1. Si desea realizar una prueba desde la línea de comandos, puede usar xcodebuild desde la línea de comandos. Por ejemplo, para probar en un simulador desde la línea de comandos, sería (en mi ejemplo, mi esquema se llama NetworkTest ):

    xcodebuild test -scheme NetworkTest -destination ''platform=iOS Simulator,name=iPhone Retina (3.5-inch),OS=7.0''

    Eso construirá el esquema y lo ejecutará en el destino especificado. Tenga en cuenta que hay muchos informes de problemas con Xcode 5.1 que prueban aplicaciones en el simulador desde la línea de comandos con xcodebuild (y puedo verificar este comportamiento, porque tengo una máquina en la que funciona bien la anterior, pero se congela en otra). Las pruebas de línea de comando contra el simulador en Xcode 5.1 no parecen ser del todo confiables.

  2. Si no desea que su prueba se ejecute en el simulador (y esto se aplica si lo hace desde la línea de comandos o desde Xcode), puede crear un objetivo de MacOS X y tener un esquema asociado para esa compilación. Por ejemplo, agregué un objetivo de Mac OS X a mi aplicación y luego agregué un esquema llamado NetworkTestMacOS para ello.

    Por cierto, si agrega un esquema de Mac OS X a un proyecto de iOS existente, es posible que las pruebas no se agreguen automáticamente al esquema, por lo que es posible que tenga que hacerlo manualmente editando el esquema, vaya a la sección de pruebas y agregue su prueba clase allí Luego puede ejecutar esas pruebas de Mac OS X desde Xcode seleccionando el esquema correcto, o también puede hacerlo desde la línea de comandos:

    xcodebuild test -scheme NetworkTestMacOS -destination ''platform=OS X,arch=x86_64''

    También tenga en cuenta que si ya ha creado su objetivo, puede ejecutar esas pruebas directamente navegando a la carpeta DerivedData correcta (en mi ejemplo, es ~/Library/Developer/Xcode/DerivedData/NetworkTest-xxx/Build/Products/Debug ) y luego ejecutando xctest directamente desde la línea de comando:

    /Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest All NetworkTestMacOSTests.xctest

  3. Otra opción para aislar sus pruebas de su sesión de Xcode es hacer sus pruebas en un servidor OS X separado. Vea la sección de Pruebas e integración continua de la prueba de video WWDC 2013 en Xcode . Para entrar en eso aquí está más allá del alcance de la pregunta original, así que simplemente lo referiré a ese video que brinda una buena introducción al tema.

Personalmente, me gusta mucho la integración de pruebas en Xcode (hace que la depuración de las pruebas sea infinitamente más fácil) y al tener un objetivo de Mac OS X, se pasa por alto el simulador en ese proceso. Pero si desea hacerlo desde la línea de comandos (o el servidor OS X), tal vez lo anterior sea útil.