Jak przetestować żądanie i odpowiedź HTTP za pomocą NSURLSession w iOS 7.1?

Chciałbym wiedzieć, jak "testować" żądania HTTP i odpowiedzi za pomocą NSURLSession. W tej chwili mój kod bloku zakończenia nie jest wywoływany, gdy działa jako test jednostkowy. Jednakże, gdy ten sam kod jest wykonywany z AppDelegate (didFinishWithLaunchingOptions), kod wewnątrz bloku zakończeń jest wywoływany. Jak sugerowano w tym wątku, nsurlsessiondatatask datataskwithurl nie jest wywoływany , użycie semaforów i / lub dispatch_group jest potrzebne "aby upewnij się, że główny wątek jest zablokowany do czasu zakończenia żądania sieciowego."

Mój kod pocztowy HTTP wygląda następująco.

@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

Metoda, która to sprawdza, wygląda następująco.

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

W moim AppDelegate kod bloku zakończenia działa i wygląda następująco.

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

Jakieś wskazówki, jak wykonać blok zakończenia podczas uruchamiania wewnątrz testu jednostkowego? Jestem nowicjuszem w iOS, więc jasny przykład naprawdę by pomógł.

  • Uwaga: I zdaj sobie sprawę, że to, o co pytam, może również nie być testowaniem "jednostkowym" w ścisłym sensie, ponieważ polegam na serwerze HTTP jako części mojego testu (co oznacza, że to, o co pytam, jest bardziej jak testowanie integracji).
  • Uwaga: zdaję sobie sprawę, że istnieje inny wątek testy jednostkowe z NSURLSession o testach jednostkowych z NSURLSession, ale nie chcę szydzić z odpowiedzi.
Author: Community, 2014-05-14

1 answers

Xcode 6 obsługuje teraz testy asynchroniczne z XCTestExpectation. Podczas testowania procesu asynchronicznego ustalasz "oczekiwanie", że proces ten zakończy się asynchronicznie, po wydaniu procesu asynchronicznego czekasz na spełnienie oczekiwań przez określoną ilość czasu, a gdy zapytanie zostanie zakończone, asynchronicznie spełnisz oczekiwania.

Na przykład:

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

Moja poprzednia odpowiedź, poniżej, poprzedni XCTestExpectation, ale zatrzymam ją dla cele historyczne.


Ponieważ twój test jest uruchomiony w kolejce głównej i ponieważ twoje żądanie jest uruchomione asynchronicznie, twój test nie będzie rejestrował zdarzeń w bloku zakończenia. Aby żądanie było synchroniczne, należy użyć semafora lub grupy dyspozytorskiej.

Na przykład:

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

SEMAFOR zapewni, że test nie zostanie zakończony, dopóki żądanie nie zostanie wykonane.

Najwyraźniej mój powyższy test to tylko losowe żądanie HTTP, ale mam nadzieję, że to ilustruje pomysł. A moje różne wypowiedzi XCTAssert będą wskazywały cztery rodzaje błędów:

  1. Obiekt NSError nie był zerowy.

  2. Kod statusu HTTP nie był 200.

  3. Obiekt NSData był zerowy.

  4. Blok ukończony nie został ukończony w ciągu 60 sekund.

Prawdopodobnie dodałbyś również testy zawartości odpowiedzi(czego nie zrobiłem w tym uproszczonym przykładzie).

Uwaga, powyższy test działa, ponieważ w moim bloku zakończeń nie ma nic, co by wysyłało cokolwiek do kolejki głównej. Jeśli testujesz to za pomocą operacji asynchronicznej, która wymaga kolejki głównej (co stanie się, jeśli nie będziesz ostrożny przy użyciu AFNetworking lub ręcznie wysyłasz do kolejki głównej samodzielnie), możesz uzyskać blokady z powyższym wzorcem (ponieważ blokujemy główny wątek czekając na zakończenie żądania sieciowego). Ale w przypadku NSURLSession Ten wzór działa świetnie.


Pytałeś o testowanie z linii poleceń, niezależnie od symulatora. Istnieje kilka aspektów:

  1. Jeśli chcesz przetestować z linii poleceń, możesz użyć xcodebuild z linii poleceń. Na przykład, aby przetestować na symulatorze z linii poleceń, byłoby to (w moim przykładzie mój schemat nazywa się NetworkTest):

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

    To zbuduje schemat i uruchomi go w określonym miejscu docelowym. Uwaga, Istnieje wiele raportów Xcode 5.1 problemy z testowaniem aplikacji na simulatorze z linii poleceń za pomocą xcodebuild (i mogę zweryfikować to zachowanie, ponieważ mam jedną maszynę, na której powyższe działa dobrze, ale zawiesza się na innej). Testowanie wiersza poleceń na symulatorze w Xcode 5.1 wydaje się nie do końca wiarygodne.

  2. Jeśli nie chcesz, aby twój test był uruchamiany na symulatorze (i dotyczy to tego, czy robi to z linii poleceń, czy z Xcode), możesz zbudować MacOS X target i mieć powiązany schemat dla ta budowa. Na przykład dodałem docelowy system Mac OS X do mojej aplikacji, a następnie dodałem do niej schemat o nazwie NetworkTestMacOS.

    BTW, jeśli dodasz Schemat systemu Mac OS X do istniejącego projektu iOS, testy mogą nie być automatycznie dodawane do schematu, więc być może będziesz musiał to zrobić ręcznie, edytując schemat, przejdź do sekcji testy i dodaj tam swoją klasę testową. Następnie można uruchomić te testy Mac OS X z Xcode, wybierając odpowiedni schemat, lub można to zrobić z wiersza poleceń, też:

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

    Zauważ również, że jeśli już zbudowałeś swój cel, możesz uruchomić te testy bezpośrednio, przechodząc do prawego folderu DerivedData (W moim przykładzie jest to ~/Library/Developer/Xcode/DerivedData/NetworkTest-xxx/Build/Products/Debug), a następnie uruchamiając xctest bezpośrednio z linii poleceń:

    /Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest All NetworkTestMacOSTests.xctest
    
  3. Inną opcją izolacji testów z sesji Xcode jest wykonanie testów na oddzielnym serwerze OS X. Zobacz ciągła integracja i testowanie sekcja wideo WWDC 2013 testowanie w Xcode . Na wejdź w to tutaj, jest znacznie poza zakresem pierwotnego pytania, więc po prostu odsyłam Cię do tego filmu, który daje miłe wprowadzenie do tematu.

Osobiście bardzo podoba mi się integracja testowania w Xcode (sprawia, że debugowanie testów jest nieskończenie łatwiejsze) i mając Mac OS X cel, można ominąć symulator w tym procesie. Ale jeśli chcesz to zrobić z linii poleceń (lub OS X Server), może powyższe pomoże.

 48
Author: Rob,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-25 01:36:48