Pull to refresh

Асинхронное юнит-тестирование в Xcode 6

Перевод статьи 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.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.