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

в 23:04, , рубрики: Песочница, метки: , ,

Перевод статьи 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.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js