2014-05-14 12 views
21

Vorrei sapere come "test dell'unità" richieste e risposte HTTP utilizzando NSURLSession. In questo momento, il mio codice di blocco di completamento non viene richiamato quando è in esecuzione come test unitario. Tuttavia, quando lo stesso codice viene eseguito all'interno di AppDelegate (didFinishWithLaunchingOptions), viene chiamato il codice all'interno del blocco di completamento. Come suggerito in questa discussione, NSURLSessionDataTask dataTaskWithURL completion handler not getting called, è necessario l'uso di semafori e/o dispatch_group "per assicurarsi che il thread principale sia bloccato fino al termine della richiesta di rete."Come posso testare le richieste e le risposte HTTP utilizzando NSURLSession in iOS 7.1?

Il mio codice postale HTTP è simile al seguente.

@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 

Il metodo che verifica questo è il seguente.

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

Nel mio AppDelegate, il codice di blocco di completamento funziona, e sembra simile alla seguente.

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

Eventuali indicazioni su come ottenere il blocco di completamento eseguito durante l'esecuzione all'interno di un test di unità? Sono un nuovo arrivato per iOS, quindi un chiaro esempio potrebbe davvero aiutare.

  • Nota: mi rendo conto di quello che sto chiedendo potrebbe anche non essere il test "unità" in senso stretto in quanto sto contando su un server HTTP come una parte del mio test (significato, che cosa sto chiedendo è più come un test di integrazione).
  • Nota: mi rendo conto che esiste un'altra discussione Unit tests with NSURLSession relativa al test delle unità con NSURLSession, ma non voglio prendere in giro le risposte.
+1

Non correlato alla tua domanda originale, quando crei la tua richiesta 'POST', ti suggerisco di evitare la percentuale dei valori (se il nome utente o la password avessero caratteri riservati, come' + 'o' & ', questo non funzionerebbe). Inoltre, è probabilmente buona pratica impostare l'intestazione 'Content-type' su' application/x-www-form-urlencoded'. Ma forse stavi solo cercando di risparmiarci da alcuni di quei dettagli cruenti. :) – Rob

risposta

48

Xcode 6 ora gestisce i test asincroni con XCTestExpectation. Quando si verifica un processo asincrono, si stabilisce "l'aspettativa" che questo processo verrà completato in modo asincrono, dopo l'emissione del processo asincrono, si attende quindi che l'aspettativa sia soddisfatta per un periodo di tempo fisso e al termine della query si eseguirà in modo asincrono soddisfare l'aspettativa.

Ad esempio:

- (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]; 
} 

La mia risposta precedente, qui di seguito, è anteriore XCTestExpectation, ma ho la conserverà per scopi storici.


Perché il test è in esecuzione sulla coda principale, e poiché la richiesta è in esecuzione in modo asincrono, il test non sarà catturare gli eventi nel blocco di completamento. Devi utilizzare un semaforo o un gruppo di distribuzione per rendere la richiesta sincrona.

Ad esempio:

- (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"); 
} 

Il semaforo farà in modo che il test non verrà completata fino a quando la richiesta fa.

Chiaramente, il mio test precedente sta solo facendo una richiesta HTTP casuale, ma si spera che illustri l'idea.E i miei vari XCTAssert dichiarazioni si identificano quattro tipi di errori:

  1. L'oggetto NSError non era pari a zero.

  2. Il codice di stato HTTP non era 200.

  3. L'oggetto NSData era pari a zero.

  4. Il blocco di completamento non è stato completato entro 60 secondi.

Si presumibilmente aggiungono anche test per il contenuto della risposta (cosa che non ho fatto in questo esempio semplificato).

Nota: il test precedente funziona perché non c'è nulla nel mio blocco di completamento che invii qualcosa alla coda principale. Se stai provando questo con un'operazione asincrona che richiede la coda principale (cosa che succederebbe se non stai facendo attenzione ad usare AFNetworking o manualmente alla coda principale tu stesso), puoi ottenere deadlock con lo schema sopra (perché stiamo bloccando il thread principale in attesa che la richiesta di rete finisca). Ma nel caso di NSURLSession, questo modello funziona alla grande.


Hai chiesto di eseguire test dalla riga di comando, indipendentemente dal simulatore. Ci sono un paio di aspetti:

  1. Se si desidera testare dalla riga di comando, è possibile utilizzare xcodebuild dalla riga di comando. Ad esempio, per testare su un simulatore dalla riga di comando, sarebbe (nel mio esempio, il mio schema è chiamato NetworkTest):

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

    che costruirà il sistema ed eseguirlo sulla destinazione specificata. Nota, ci sono molte segnalazioni di problemi Xcode 5.1 testare le app sul simulatore dalla riga di comando con xcodebuild (e posso verificare questo comportamento, perché ho una macchina su cui funziona quanto sopra, ma si blocca su un'altra macchina). Il test da riga di comando contro il simulatore in Xcode 5.1 sembra non del tutto affidabile.

  2. Se non si desidera eseguire il test sul simulatore (e questo si applica sia dalla riga di comando o da Xcode), è possibile creare un target MacOS X e disporre di uno schema associato per tale costruire. Ad esempio, ho aggiunto un obiettivo Mac OS X alla mia app, quindi ho aggiunto uno schema chiamato NetworkTestMacOS per questo.

    BTW, se si aggiunge uno schema Mac OS X a un progetto iOS esistente, i test potrebbero non essere aggiunti automaticamente allo schema, quindi potrebbe essere necessario farlo manualmente modificando lo schema, accedere alla sezione dei test, e aggiungi la tua lezione di test lì.È quindi possibile eseguire questi test da Mac OS X. Xcode dalla scelta del sistema di destra, o si può fare dalla riga di comando, anche:

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

    Si noti inoltre, se hai già costruito la vostra destinazione, è possibile eseguire questi test direttamente navigando alla cartella destra DerivedData (nel mio esempio, è ~/Library/Developer/Xcode/DerivedData/NetworkTest-xxx/Build/Products/Debug) e quindi eseguire xctest direttamente dalla linea di comando:

     
    /Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest All NetworkTestMacOSTests.xctest 
    
  3. Un'altra opzione per isolare i test dalla sessione di Xcode è quello di fare il tuo test su un OS X Server separato. Vedere la sezione Continuous Integration and Testing del video WWDC 2013 Testing in Xcode. Entrare in questo punto va ben oltre lo scopo della domanda originale, quindi ti rimanderò semplicemente a quel video che offre una bella introduzione all'argomento.

Personalmente, mi piace molto l'integrazione di test in Xcode (rende la messa a punto di test infinitamente più facile) ed avendo un obiettivo di Mac OS X, si ignora il simulatore in questo processo. Ma se vuoi farlo dalla riga di comando (o OS X Server), forse il sopra aiuta.

+0

Esiste un modo per eseguire la suite/i casi di test al di fuori di XCode e/o del simulatore (ad esempio dalla riga di comando)? Vengo dal mondo Java in cui hai creato strumenti per rendere il test indipendente da un simulatore. –

+0

@JaneWayne Se si desidera rendere il test indipendente dal simulatore, creare un target Mac OS X e testarlo. Se vuoi rendere il test indipendente da Xcode, puoi usare gli strumenti della riga di comando (ma non sono sicuro che tu voglia davvero andarci). Ad ogni modo, vedi la mia risposta rivista. – Rob

+0

Anche questo copre questo sono Swift Mocking ... molto bello! http://nshipster.com/xctestcase/ – DogCoffee