Как выполнить модульное тестирование HTTP-запроса и ответа с помощью NSURLSession в iOS 7.1?

Я хотел бы знать, как «модульное тестирование» HTTP-запросов и ответов с использованием NSURLSession. Прямо сейчас мой код блока завершения не вызывается при запуске в качестве модульного теста. Однако когда тот же код выполняется из 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];
}

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

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

person Jane Wayne    schedule 14.05.2014    source источник
comment
Независимо от вашего первоначального вопроса, при создании вашего запроса POST я бы предложил процентное экранирование значений (если имя пользователя или пароль содержат зарезервированные символы, такие как + или &, это не сработает). Кроме того, вероятно, хорошей практикой будет установить для заголовка Content-type значение application/x-www-form-urlencoded. Но, возможно, вы просто пытались избавить нас от некоторых кровавых подробностей. :)   -  person Rob    schedule 14.05.2014


Ответы (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 будут идентифицировать четыре типа ошибок:

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

  2. Код состояния HTTP не был 200.

  3. Объект NSData был нулевым.

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

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

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


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

  1. Если вы хотите протестировать из командной строки, вы можете использовать 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 кажется не совсем надежным.

  2. Если вы не хотите, чтобы ваш тест выполнялся на симуляторе (и это применимо независимо от того, выполняется ли он из командной строки или из 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
    
  3. Другой вариант изоляции ваших тестов от вашего сеанса Xcode — это тестирование на отдельном сервере OS X. См. раздел Непрерывная интеграция и тестирование видео WWDC 2013 Testing. в Xcode. Вдаваться в это здесь выходит далеко за рамки первоначального вопроса, поэтому я просто отсылаю вас к этому видео, которое дает хорошее введение в тему.

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

person Rob    schedule 14.05.2014
comment
Есть ли способ запустить набор тестов/случаи вне XCode и/или симулятора (например, из командной строки)? Я пришел из мира Java, где у вас есть инструменты для сборки, чтобы сделать тестирование независимым от симулятора. - person Jane Wayne; 16.05.2014
comment
@JaneWayne Если вы хотите сделать тестирование независимым от симулятора, создайте цель Mac OS X и протестируйте ее. Если вы хотите сделать тестирование независимым от самого Xcode, вы можете использовать инструменты командной строки (но я не уверен, что вы действительно хотите туда идти). В любом случае, смотрите мой пересмотренный ответ. - person Rob; 16.05.2014
comment
Это также касается Swift Mocking... очень круто! nshipster.com/xctestcase - person DogCoffee; 08.09.2015
comment
Спасибо за этот отличный ответ. В моем случае мне не хватало [резюме задачи]; call и поэтому мой тест не дождался блока завершения! - person C0D3; 09.11.2016