Подтвердить что ты не робот

Как unit test HTTP-запрос и ответ с использованием NSURLSession в iOS 7.1?

Я хотел бы знать, как "unit test" HTTP-запросы и ответы, используя NSURLSession. Прямо сейчас мой код блока завершения не вызывается при запуске как unit test. Однако, когда тот же код выполняется из AppDelegate (didFinishWithLaunchingOptions), вызывается код внутри блока завершения. Как было предложено в этой статье, NSURLSessionDataTask dataTaskWithURL обработчик завершения, который не получает вызов, необходимо использовать семафоры и/или dispatch_group ", чтобы убедиться, что основной поток заблокирован до тех пор, пока сеть запрос заканчивается."

Мой почтовый HTTP-код выглядит следующим образом.

@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

Метод, который проверяет это, выглядит следующим образом.

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

В моем AppDelegate код блока завершения работает, и он выглядит следующим образом.

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

Любые указатели на то, как получить блок завершения, выполняемый при работе внутри unit test? Я новичок в iOS, поэтому ясный пример действительно поможет.

  • Примечание. Я понимаю, что я прошу, также не может быть "единичным" тестированием в строгом смысле в том смысле, что я полагаюсь на HTTP-сервер в качестве части моего теста (это значит, что я прошу больше походить на интеграцию тестирование).
  • Примечание. Я понимаю, что есть еще один поток Unit тесты с NSURLSession об модульном тестировании с помощью NSURLSession, но я не хочу издеваться над ответами.
4b9b3361

Ответ 1

Теперь Xcode 6 обрабатывает асинхронные тесты с помощью XCTestExpectation. При тестировании асинхронного процесса вы устанавливаете "ожидание", что этот процесс завершится асинхронно, после выдачи асинхронного процесса, вы ждете, пока ожидание будет удовлетворено в течение фиксированного периода времени, и когда запрос завершится, вы будете асинхронно выполнить ожидание.

Например:

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

Мой предыдущий ответ ниже, предшествует XCTestExpectation, но я сохраню его в исторических целях.


Поскольку ваш тест выполняется в главной очереди, и поскольку ваш запрос выполняется асинхронно, ваш тест не будет захватывать события в блоке завершения. Вы должны использовать семафор или группу отправки, чтобы сделать запрос синхронным.

Например:

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

Семафор гарантирует, что тест не будет завершен до тех пор, пока запрос не выполнит.

Ясно, что мой вышеприведенный тест просто делает случайный HTTP-запрос, но, надеюсь, он иллюстрирует идею. И мои различные операторы XCTAssert идентифицируют четыре типа ошибок:

  • Объект NSError не был нулевым.

  • Код статуса HTTP не был 200.

  • Объект NSData был равен нулю.

  • Блок завершения не завершился в течение 60 секунд.

Предположительно вы также добавите тесты для содержимого ответа (чего я не делал в этом упрощенном примере).

Обратите внимание, что вышеупомянутый тест работает, потому что в моем блоке завершения ничего не происходит, что отправляет что-либо в основную очередь. Если вы тестируете это с помощью асинхронной операции, для которой требуется основная очередь (что произойдет, если вы не будете осторожны при использовании AFNetworking или вручную отправляете в основную очередь самостоятельно), вы можете получить взаимоблокировки с указанным выше шаблоном (поскольку мы блокируем основной поток ожидает завершения запроса сети). Но в случае NSURLSession этот шаблон отлично работает.


Вы спросили о проведении тестирования из командной строки, независимо от симулятора. Существует несколько аспектов:

  • Если вы хотите проверить из командной строки, вы можете использовать xcodebuild из командной строки. Например, чтобы проверить на симуляторе из командной строки, это будет (в моем примере моя схема называется NetworkTest):

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

    Это построит схему и запустит ее в указанном месте назначения. Обратите внимание: есть много сообщений о проблемах Xcode 5.1, которые тестируют приложения на симуляторе из командной строки с помощью xcodebuild (и я могу проверить это поведение, потому что у меня есть одна машина, на которой это работает отлично, но она зависает на другом). Тест командной строки против симулятора в Xcode 5.1 выглядит не совсем надежным.

  • Если вы не хотите, чтобы ваш тест запускался на симуляторе (и это относится к тому, выполняются ли они из командной строки или из Xcode), вы можете создать целевую систему MacOS X и иметь соответствующую схему для которые строят. Например, я добавил целевое приложение Mac OS X для своего приложения, а затем добавил для него схему под названием NetworkTestMacOS.

    Кстати, если вы добавите схему Mac OS X и существующий проект iOS, тесты могут не добавляться автоматически к схеме, поэтому вам, возможно, придется это сделать вручную, отредактировав схему, перейдите в раздел "Тесты" и добавьте свой тестовый класс. Затем вы можете запустить эти тесты Mac OS X из Xcode, выбрав правильную схему или вы также можете сделать это из командной строки:

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

    Также обратите внимание: если вы уже создали свою цель, вы можете запустить эти тесты напрямую, перейдя в правую папку DerivedData (в моем примере, она ~/Library/Developer/Xcode/DerivedData/NetworkTest-xxx/Build/Products/Debug), а затем запустив xctest непосредственно из команды строка:

    /Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest All NetworkTestMacOSTests.xctest
    
  • Еще один вариант для изоляции ваших тестов с вашего сеанса Xcode - это выполнить тестирование на отдельном OS X Server. См. Раздел "Непрерывная интеграция и тестирование" видеоролика WWDC 2013 Тестирование в Xcode. Чтобы войти в то, что здесь выходит далеко за рамки исходного вопроса, поэтому я просто отправлю вас к тому видео, которое дает хорошее введение в эту тему.

Лично мне очень нравится интеграция тестирования в Xcode (это делает отладку тестов бесконечно проще), и имея цель Mac OS X, вы обходите симулятор в этом процессе. Но если вы хотите сделать это из командной строки (или OS X Server), возможно, это поможет.