Как модульный тест асинхронных API?

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

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

Как бы я проверил это как модульный тест?

похоже, я хотел бы, чтобы функция 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");
}

13 ответов


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

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

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

в делегате(с).


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

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

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

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

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

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

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

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


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

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


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


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

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

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


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

тест:

// create request operation

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

// verify response

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


чтобы разработать решение @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;
        }
    }

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

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

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

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

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


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

- (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. 
}

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



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

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


Я нашел эту статью об этом, которая является muc http://dadabeatnik.wordpress.com/2013/09/12/xcode-and-asynchronous-unit-testing/