Как unit test асинхронные API?

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

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

Как я могу проверить это как unit test?

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

EDIT: теперь я переключился на использование встроенной XCTest-системы XCode и обнаружил, что 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");
}

Ответ 1

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

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

// 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];

В делегате (-ах) затем.

Ответ 2

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

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

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

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

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

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

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

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

Ответ 3

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

Запуск цикла может потенциально запускаться вечно, конечно. Я улучшу это позже!

Ответ 4

Я написал небольшой помощник, который позволяет легко тестировать асинхронный 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, поэтому обязательно установите его после завершения. Конечно, вы можете добавить тайм-аут к помощнику, если хотите,

Ответ 5

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

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

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

Ответ 6

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

В тесте:

// create request operation

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

// verify response

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

Ответ 7

Чтобы разработать решение @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 no response - timeout after some time
        if(--timeoutCounter <= 0)
        {
            break;
        }
    }

Ответ 8

Мне очень удобно использовать 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];
}

Ответ 9

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

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

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

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

Ответ 10

Я только что написал запись в блоге об этом (на самом деле я начал блог, потому что думал, что это интересная тема). Я закончил использовать метод swizzling, поэтому я могу вызвать обработчик завершения, используя любые аргументы, которые я хочу, не дожидаясь, что показалось хорошим для модульного тестирования. Что-то вроде этого:

- (void)swizzledGeocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler
{
    completionHandler(nil, nil); //You can test various arguments for the handler here.
}

- (void)testGeocodeFlagsComplete
{
    //Swizzle the geocodeAddressString with our own method.
    Method originalMethod = class_getInstanceMethod([CLGeocoder class], @selector(geocodeAddressString:completionHandler:));
    Method swizzleMethod = class_getInstanceMethod([self class], @selector(swizzledGeocodeAddressString:completionHandler:));
    method_exchangeImplementations(originalMethod, swizzleMethod);

    MyGeocoder * myGeocoder = [[MyGeocoder alloc] init];
    [myGeocoder geocodeAddress]; //the completion handler is called synchronously in here.

    //Deswizzle the methods!
    method_exchangeImplementations(swizzleMethod, originalMethod);

    STAssertTrue(myGeocoder.geocoded, @"Should flag as geocoded when complete.");//You can test the completion handler code here. 
}

запись в блоге для тех, кто интересуется.

Ответ 12

Отвечаю, что модульное тестирование концептуально не подходит для тестирования асинхронных операций. Асинхронная операция, такая как запрос на сервер и обработка ответа, происходит не в одном блоке, а в двух единицах.

Чтобы связать ответ с запросом, вы должны либо как-то блокировать выполнение между двумя единицами, либо поддерживать глобальные данные. Если вы блокируете выполнение, ваша программа не выполняется нормально, и если вы поддерживаете глобальные данные, вы добавляете посторонние функции, которые могут содержать ошибки. Любое решение нарушает всю идею модульного тестирования и требует, чтобы вы ввели специальный код тестирования в свое приложение; а затем после тестирования вашего устройства вам все равно придется отключить свой тестовый код и выполнить старое "ручное" тестирование. Время и усилия, затраченные на модульное тестирование, затем, по крайней мере, частично теряются.