Как модульное тестирование асинхронных API?
Я установил Google Toolbox для Mac в Xcode и следуйте инструкциям по настройке модульного тестирования найдено здесь.
все это отлично работает, и я могу проверить свои синхронные методы на всех моих объектах абсолютно нормально. Тем не менее, большинство сложных API, которые я действительно хочу проверить, возвращают результаты асинхронно, вызывая метод на делегате - например, вызов системы загрузки и обновления файлов немедленно вернется, а затем запустите - fileDownloadDidComplete: метод, когда файл завершает загрузку.
Как бы я проверил это как модульный тест?
похоже, я хотел бы, чтобы функция testDownload или, по крайней мере, тестовая платформа "ждала" для запуска метода fileDownloadDidComplete:.
EDIT: теперь я переключился на использование встроенной системы 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 ответов:
я столкнулся с тем же вопросом и нашли другое решение, которое работает для меня.
Я использую подход "старой школы" для превращения асинхронных операций в поток синхронизации с помощью семафора следующим образом:
// 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];
в делегате(с).
Я ценю, что этот вопрос был задан и ответил почти год назад, но я не могу не согласиться с данными ответами. Тестирование асинхронных операций, в частности сетевых операций, является очень распространенным требованием, и важно, чтобы получить право. В данном примере, если вы зависите от реальных сетевых ответов, вы теряете часть важной ценности ваших тестов. В частности, ваши тесты становятся зависимыми от наличия и корректности функционирования сервера общение с; Эта зависимость делает ваши тесты
- более хрупкими (что произойдет, если сервер выйдет из строя?)
- менее полный (как вы последовательно тестируете ответ на сбой или сетевую ошибку?)
- значительно медленнее представьте себе тестирование этого:
модульные тесты должны выполняться за доли секунды. Если вам приходится ждать многосекундного ответа сети каждый раз, когда вы запускаете свои тесты, то вы с меньшей вероятностью запустите их часто.
модульное тестирование в основном связано с инкапсулированием зависимостей; с точки зрения вашего тестируемого кода происходят две вещи:
- ваш метод инициирует сетевой запрос, вероятно, путем создания экземпляра NSURLConnection.
- указанный делегат получает ответ через определенные вызовы методов.
ваш делегат не должен или не должен заботиться о том, откуда пришел ответ, будь то от фактического ответа от a удаленный сервер или из вашего тестового кода. Вы можете воспользоваться этим для тестирования асинхронных операций, просто генерируя ответы самостоятельно. Ваши тесты будут выполняться намного быстрее, и вы можете надежно проверить ответы на успех или неудачу.
Это не значит, что вы не должны запускать тесты против реальной веб-службы, с которой вы работаете, но это интеграционные тесты и принадлежат к их собственному набору тестов. Сбои в этом наборе могут означать, что веб-служба имеет изменения или просто не работает. Поскольку они более хрупкие, их автоматизация имеет меньшую ценность, чем автоматизация ваших модульных тестов.
Что касается того, как именно тестировать асинхронные ответы на сетевой запрос, у вас есть несколько вариантов. Вы можете просто проверить делегат в изоляции, вызвав методы напрямую (например, [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 для s короткой продолжительности в цикле. И пусть обратный вызов устанавливает некоторую общую переменную состояния. Или, может быть, даже просто попросите обратный вызов завершить runloop. Таким образом, вы знаете, что тест закончился. Вы должны быть в состоянии проверить таймауты путем остановки цикла через определенное время. Если это произойдет, то это стоило ожидания.
Я никогда не делал этого, но мне скоро придется, я думаю. Пожалуйста, поделитесь своими результатами :-)
Если вы используете библиотеку, такую как 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, поэтому я могу вызвать обработчик завершения, используя любые аргументы, которые я хочу, не дожидаясь, что казалось хорошим для модульного тестирования. Что-то вроде этого:
- (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/