Как выполнить модульное тестирование асинхронных API? - PullRequest
64 голосов
/ 29 января 2010

Я установил Google Toolbox для Mac в Xcode и следовал инструкциям по настройке модульного тестирования, найденных здесь .

Все это прекрасно работает, и я могу тестировать свои синхронные методы на всех моих объектах абсолютно нормально. Однако большинство сложных API-интерфейсов, которые я на самом деле хочу протестировать, возвращают результаты асинхронно, вызывая метод для делегата - например, вызов системы загрузки и обновления файлов сразу же возвращается, а затем запускает -fileDownloadDidComplete: метод после завершения загрузки файла. .

Как бы я проверил это как юнит-тест?

Похоже, я бы хотел, чтобы функция testDownload или, по крайней мере, тестовая среда ожидала запуска fileDownloadDidComplete: метод.

РЕДАКТИРОВАТЬ: Теперь я переключился на использование встроенной системы XCode XCTest и обнаружил, что TVRSMonitor на Github предоставляет простой и удобный способ использования семафоров для ожидания завершения асинхронных операций.

Например:

- (void)testLogin {
  TRVSMonitor *monitor = [TRVSMonitor monitor];
  __block NSString *theToken;

  [[Server instance] loginWithUsername:@"foo" password:@"bar"
                               success:^(NSString *token) {
                                   theToken = token;
                                   [monitor signal];
                               }

                               failure:^(NSError *error) {
                                   [monitor signal];
                               }];

  [monitor wait];

  XCTAssert(theToken, @"Getting token");
}

Ответы [ 13 ]

52 голосов
/ 11 апреля 2010

Я столкнулся с тем же вопросом и нашел другое решение, которое работает для меня.

Я использую подход «старой школы» для преобразования асинхронных операций в поток синхронизации с помощью семафора следующим образом:

// create the object that will perform an async operation
MyConnection *conn = [MyConnection new];
STAssertNotNil (conn, @"MyConnection init failed");

// create the semaphore and lock it once before we start
// the async operation
NSConditionLock *tl = [NSConditionLock new];
self.theLock = tl;
[tl release];    

// start the async operation
self.testState = 0;
[conn doItAsyncWithDelegate:self];

// now lock the semaphore - which will block this thread until
// [self.theLock unlockWithCondition:1] gets invoked
[self.theLock lockWhenCondition:1];

// make sure the async callback did in fact happen by
// checking whether it modified a variable
STAssertTrue (self.testState != 0, @"delegate did not get called");

// we're done
[self.theLock release]; self.theLock = nil;
[conn release];

Обязательно вызовите

[self.theLock unlockWithCondition:1];

У делегата (ов) тогда.

44 голосов
/ 15 ноября 2010

Я ценю, что на этот вопрос задавали и отвечали почти год назад, но я не могу не согласиться с данными ответами. Тестирование асинхронных операций, особенно сетевых операций, является очень распространенным требованием и важно, чтобы получить правильные результаты. В данном примере, если вы зависите от реальных сетевых ответов, вы теряете некоторые важные значения ваших тестов. В частности, ваши тесты становятся зависимыми от доступности и функциональной корректности сервера, с которым вы общаетесь; эта зависимость делает ваши тесты

  • более хрупкий (что произойдет, если сервер выйдет из строя?)
  • менее полно (как вы последовательно тестируете реакцию на ошибку или сетевую ошибку?)
  • значительно медленнее, представьте себе тестирование:

Модульные тесты должны выполняться за доли секунды. Если вам приходится каждый раз ждать, пока вы будете выполнять тесты, ждать ответа по сети в течение нескольких секунд, то вы вряд ли будете часто их запускать.

Модульное тестирование в основном сводится к инкапсуляции зависимостей; с точки зрения тестируемого кода происходят две вещи:

  1. Ваш метод инициирует сетевой запрос, возможно, путем создания NSURLConnection.
  2. Указанный вами делегат получает ответ через определенные вызовы методов.

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

Это не значит, что вы не должны запускать тесты для реального веб-сервиса, с которым вы работаете, но это интеграционные тесты, которые принадлежат их собственному тестовому набору. Сбои в этом наборе могут означать, что веб-служба изменилась или просто не работает. Поскольку они более хрупкие, их автоматизация имеет меньшую ценность, чем автоматизация ваших юнит-тестов.

Относительно того, как именно выполнить тестирование асинхронных ответов на сетевой запрос, у вас есть несколько вариантов. Вы можете просто протестировать делегат изолированно, вызывая методы напрямую (например, [someDelegate connection: connection didReceiveResponse: someResponse]). Это будет работать несколько, но немного неправильно. Делегат, предоставляемый вашим объектом, может быть только одним из нескольких объектов в цепочке делегатов для конкретного объекта NSURLConnection; если вы вызываете методы вашего делегата напрямую, вам может не хватать какой-то ключевой части функциональности, предоставляемой другим делегатом в дальнейшей цепочке. В качестве лучшей альтернативы вы можете создать заглушку созданного вами объекта NSURLConnection и отправить ему ответные сообщения по всей цепочке делегатов. Существуют библиотеки, которые откроют NSURLConnection (среди других классов) и сделают это для вас. Например: https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

19 голосов
/ 29 января 2010

St3fan, ты гений. Большое спасибо!

Вот как я это сделал, используя ваше предложение.

'Downloader' определяет протокол с методом DownloadDidComplete, который запускается при завершении. Существует переменная-член BOOL 'downloadComplete', которая используется для завершения цикла выполнения.

-(void) testDownloader {
 downloadComplete = NO;
 Downloader* downloader = [[Downloader alloc] init] delegate:self];

 // ... irrelevant downloader setup code removed ...

 NSRunLoop *theRL = [NSRunLoop currentRunLoop];

 // Begin a run loop terminated when the downloadComplete it set to true
 while (!downloadComplete && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

}


-(void) DownloaderDidComplete:(Downloader*) downloader withErrors:(int) errors {
    downloadComplete = YES;

    STAssertNotEquals(errors, 0, @"There were errors downloading!");
}

Цикл выполнения потенциально может работать вечно ... Я исправлю это позже!

16 голосов
/ 31 декабря 2012

Я написал небольшой помощник, который облегчает тестирование асинхронного API. Первый помощник:

static inline void hxRunInMainLoop(void(^block)(BOOL *done)) {
    __block BOOL done = NO;
    block(&done);
    while (!done) {
        [[NSRunLoop mainRunLoop] runUntilDate:
            [NSDate dateWithTimeIntervalSinceNow:.1]];
    }
}

Вы можете использовать это так:

hxRunInMainLoop(^(BOOL *done) {
    [MyAsyncThingWithBlock block:^() {
        /* Your test conditions */
        *done = YES;
    }];
});

Это будет продолжаться, только если done станет TRUE, поэтому обязательно установите его после завершения. Конечно, вы можете добавить тайм-аут к помощнику, если хотите,

8 голосов
/ 29 января 2010

Это сложно. Я думаю, что вам нужно будет установить runloop в вашем тесте, а также возможность указать этот runloop для вашего асинхронного кода. В противном случае обратные вызовы не будут выполняться, поскольку они выполняются в цикле выполнения.

Полагаю, вы могли бы просто запустить цикл цикла на короткое время в цикле. И пусть обратный вызов устанавливает некоторую общую переменную состояния. Или, может быть, даже просто попросить обратный вызов, чтобы завершить цикл запуска. Таким образом, вы знаете, что тест закончен. Вы должны быть в состоянии проверить время ожидания, остановив цикл через определенное время. Если это произойдет, то истекло время ожидания.

Я никогда не делал этого, но мне скоро придется, я думаю. Пожалуйста, поделитесь своими результатами: -)

6 голосов
/ 20 мая 2012

Чтобы уточнить решение @ St3fan, вы можете попробовать это после инициирования запроса:

- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs
{
    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];

    do
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
        if ([timeoutDate timeIntervalSinceNow] < 0.0)
        {
            break;
        }
    }
    while (!done);

    return done;
}

Другой способ:

//block the thread in 0.1 second increment, until one of callbacks is received.
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];

    //setup timeout
    float waitIncrement = 0.1f;
    int timeoutCounter  = (int)(30 / waitIncrement); //30 sec timeout
    BOOL controlConditionReached = NO;


    // Begin a run loop terminated when the downloadComplete it set to true
    while (controlConditionReached == NO)
    {

        [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:waitIncrement]];
        //control condition is set in one of your async operation delegate methods or blocks
        controlConditionReached = self.downloadComplete || self.downloadFailed ;

        //if there's no response - timeout after some time
        if(--timeoutCounter <= 0)
        {
            break;
        }
    }
6 голосов
/ 17 мая 2012

Если вы используете библиотеку, такую ​​как AFNetworking или ASIHTTPRequest, и ваши запросы обрабатываются с помощью NSOperation (или подкласса этих библиотек), то их легко протестировать на сервере test / dev с NSOperationQueue:

В тесте:

// create request operation

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:request];
[queue waitUntilAllOperationsAreFinished];

// verify response

Это, по сути, запускает цикл запуска до завершения операции, позволяя выполнять все обратные вызовы в фоновых потоках, как обычно.

3 голосов
/ 09 апреля 2014

Мне очень удобно пользоваться https://github.com/premosystems/XCAsyncTestCase

Он добавляет три очень удобных метода в XCTestCase

@interface XCTestCase (AsyncTesting)

- (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout;
- (void)waitForTimeout:(NSTimeInterval)timeout;
- (void)notify:(XCTAsyncTestCaseStatus)status;

@end

, которые позволяют очень чистые тесты. Пример из самого проекта:

- (void)testAsyncWithDelegate
{
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
    [NSURLConnection connectionWithRequest:request delegate:self];
    [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:10.0];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Request Finished!");
    [self notify:XCTAsyncTestCaseStatusSucceeded];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"Request failed with error: %@", error);
    [self notify:XCTAsyncTestCaseStatusFailed];
}
2 голосов
/ 10 января 2013

Я реализовал решение, предложенное Томасом Темпельманном, и в целом оно отлично работает для меня.

Однако, есть ошибка. Предположим, что тестируемое устройство содержит следующий код:

dispatch_async(dispatch_get_main_queue(), ^{
    [self performSelector:selector withObject:nil afterDelay:1.0];
});

Селектор никогда не может быть вызван, так как мы сказали основному потоку заблокировать, пока тест не завершится:

[testBase.lock lockWhenCondition:1];

В целом, мы можем полностью избавиться от NSConditionLock и просто использовать класс GHAsyncTestCase .

Вот как я использую его в своем коде:

@interface NumericTestTests : GHAsyncTestCase { }

@end

@implementation NumericTestTests {
    BOOL passed;
}

- (void)setUp
{
    passed = NO;
}

- (void)testMe {

    [self prepare];

    MyTest *test = [MyTest new];
    [test run: ^(NSError *error, double value) {
        passed = YES;
        [self notify:kGHUnitWaitStatusSuccess];
    }];
    [test runTest:fakeTest];

    [self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0];

    GHAssertTrue(passed, @"Completion handler not called");
}

Значительно чище и не блокирует основной поток.

1 голос
/ 15 сентября 2014
...