Перевод статьи Asynchronous Unit Testing in Xcode 6 Phil Beauvoir-а
В прошлом году я описал метод для реализации асинхронного юнит-тестирования в Xcode 5.
Давайте вспомним, какие есть проблемы с асинхронным юнит-тестированием. Множество API на платформе IOS сами по себе являются асинхронными. Они используют механизмы обратного вызова, чтобы посигналить когда закончат, и при этому могут быть в различных очередях. Они могут создавать запросы к сети или записывать в локальные системные файлы. Они могут быть длительными задачами, которые требуется запускать в фоне. Это создает проблемы, потому что тестирование само по себе запускается асинхронно. Поэтому наши тесты должны подождать пока их уведомят о том, что запущенная задача выполнена.
Я предложил метод, который требует установки логического флага (boolean flag) в юнит-тесте и зацикливания петлей while() до тех пор, пока флаг не будет установлен в false, позволяя, тем самым, тесту выполнить условия. Этот метод работает почти всегда, но я никогда не был им доволен, считая его отчасти костылем. В том посте я пришел к выводу:
у меня все также остались сомнения об этой технике, и я продолжаю искать идеальное решения для асинхронного юнит-тестирования в Xcode. Возможно Apple дала решение в XCTest, может быть, что-то подобное реализовано в GHUnit.
Вот пример Objecive-C версий скелета асинхронного юнит-теста в Xcode 5, использующего старый метод:
- (void)testSaveAndCreateDocument {
NSURL *url = ...; // URL к файлу
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url];
// Ставим флаг в значение YES
__block BOOL waitingForBlock = YES;
// Вызываем асинхронный метод с обработчиком завершения
[document saveToURL:document.fileURL
forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
// Ставим флаг в NO, чтобы прервать цикл
waitingForBlock = NO;
// Assert the truth
STAssertTrue(success, @"Should have been success!");
}];
// Запускаем цикл
while(waitingForBlock) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
}
Фактически, потому что я повторно использую тот же самый паттерн в большом количестве тестов, я конвертировал в Macro части, которые должны быть включены в каждый хэдер-файл. Кроме того, я заметил, что при некоторых условиях тест не оправдывает ожидания.
Но хорошей новостью стало то, что меньше, чем через год Apple поставила средства для реализации асинхронных юнит-тестов разумным и официально поддерживаемым способом. Кроме того, компания не только дала нам новую версию Xcode 6 с новым фреймворком юнит-тестирования, но также представила совершенно новый язык программирования Swift. Я потратил некоторое время на протяжении последних нескольких недель, конвертируя здоровенный кусок кода из Objective-C в Swift, и конвертируя мои Юнит-Тесты в XCTest фреймворк, я внедрил новые методы Apple для асинхронного юнит-тестирования. Теперь всё мое программирование будет выполнятся на Swift, поэтому пример ниже будет также на нем.
Ну и как это работает? В Xcode 6 Apple добавила некоторые расширения для класса XCTestCase, и я сфокусируюсь на двух из них:
// ожидание с описанием
func expectationWithDescription(description: String!) -> XCTestExpectation!
// ожидание события с задержкой
func waitForExpectationsWithTimeout(timeout: NSTimeInterval, handler handlerOrNil: XCWaitCompletionHandler!)
Здесь также присутствует новый класс XCTestExpectation, который имеет один метод
class XCTestExpectation : NSObject {
func fulfill()
}
В основном, вы объявляете «ожидание»(expectation) в вашем юнит-тесте, и цикл в цикле ожидания ждет когда в вашем коде сработает это ожидание (expectation). Это такой же паттерн как и до этого, но с большим количеством опций. Ниже приводится старый Objective-C-шный код, переделанный в Swift для нового фреймворка:
func testSaveAndCreateDocument() {
let url = NSURL.URLWithString("path-to-file")
let document = UIManagedDocument(fileURL: url)
// Объявляем наше ожидание
let readyExpectation = expectationWithDescription("ready")
// Вызываем асинхронный метод с обработчиком завершения
document.saveToURL(url, forSaveOperation: UIDocumentSaveOperation.ForCreating, completionHandler: { success in
// Выполняем наши тесты...
XCTAssertTrue(success, "saveToURL failed")
// И завершаем ожидание...
readyExpectation.fulfill()
})
// Ждем пока не завершится наше ожидание
waitForExpectationsWithTimeout(5, { error in
XCTAssertNil(error, "Error")
})
}
Мы инстанцировали новый экземпляр XCTestExpectation, названный readyExpectation. Мы дали ему простое описание «ready» для удобства. Оно будет отображаться в логах теста, чтобы помочь выявить сбои. Возможно также задать больше одного ожидания в качестве условия. Затем мы вызвали код, который должен быть протестирован. В обработчике завершения, после создания нашего теста, мы вызываем метод fulfill() у нашего ожидания. Это эквивалентно установке флага false в нашем, ранее реализованном Objective-C коде.
Последний блок кода запускает цикл, выполняемый до тех пор, пока все ожидания не будут завершены, или пока время не истечет. Я выставил таймаут в 5 секунд, чтобы не рисковать.
Вот и все! Есть еще много всего, что вы сможете сделать с новыми дополнениями в юнит-тестировании, например KVO и показатели производительности, но то, что было описано выше — достаточно, чтобы начать работать. Наконец-то у нас есть надлежащий фреймворк для асинхронного юнит-тестирования в Xcode.