Как я могу заставить XCTest ждать асинхронных вызовов в setUp перед запуском тестов?

Я пишу интеграционные тесты в Xcode 6 вместе с модульными и функциональными тестами. XCTest имеет метод setUp (), который вызывается перед каждым тестом. Здорово!

Также есть исключения XCTestException, которые позволяют мне писать асинхронные тесты. Также отлично!

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

Есть ли способ заставить setUp дождаться готовности моей базы данных перед запуском тестов?

Вот пример того, чем я занимаюсь сейчас. Поскольку setUp возвращается до того, как база данных будет заполнена, мне нужно дублировать много тестового кода для каждого теста:

func test_checkSomethingExists() {

    let expectation = expectationWithDescription("")
    var expected:DatabaseItem

    // Fill out a database with data. 
    var data = getData()
    overwriteDatabase(data, {
      // Database populated.
      // Do test... in this pseudocode I just check something...
      db.retrieveDatabaseItem({ expected in

        XCTAssertNotNil(expected)

        expectation.fulfill()
      })
    })

    waitForExpectationsWithTimeout(5.0) { (error) in
        if error != nil {
            XCTFail(error.localizedDescription)
        }
    }

}

Вот чего бы я хотел:

class MyTestCase: XCTestCase {

    override func setUp() {
        super.setUp()

        // Fill out a database with data. I can make this call do anything, here
        // it returns a block.
        var data = getData()
        db.overwriteDatabase(data, onDone: () -> () {

           // When database done, do something that causes setUp to end 
           // and start running tests

        })        
    }

    func test_checkSomethingExists() {

        let expectation = expectationWithDescription("")
        var expected:DatabaseItem


          // Do test... in this pseudocode I just check something...
          db.retrieveDatabaseItem({ expected in

            XCTAssertNotNil(expected)

            expectation.fulfill()
        })

        waitForExpectationsWithTimeout(5.0) { (error) in
            if error != nil {
                XCTFail(error.localizedDescription)
            }
        }

    }

}

person Brett Elliot    schedule 08.04.2015    source источник
comment
Если вы выполните поиск по переполнению стека для асинхронного модульного теста [ios], вы увидите массу ответов не только с техникой XCTestExpectation (не XCTestException), но и с техникой семафоров. например stackoverflow.com/a/23658385/1271826. Вы, вероятно, можете использовать технику семафоров для своего асинхронного кода базы данных (хотя вы не рассказали, как вы делаете этот материал базы данных, поэтому мы не можем быть более конкретными, чем это). Я удивлен, что в вашей библиотеке баз данных нет функции синхронизации, потому что это очень часто встречается в библиотеках баз данных.   -  person Rob    schedule 08.04.2015
comment
Роб, я отредактировал свой вопрос, чтобы показать именно то, что я ищу. Я знаю, как использовать XCTest и XCTestException для написания асинхронных тестов. Я не знаю, как предотвратить запуск тестов до завершения setUp. Спасибо.   -  person Brett Elliot    schedule 08.04.2015
comment
Ржу не могу. Опять же, нет такой вещи, как XCTestException. Это XCTestExpectation. И, как я уже сказал, используйте технику семафоров в setUp, а не в XCTestExpectation. (Используйте ожидания в тестах, но в setUp используйте семафоры.)   -  person Rob    schedule 08.04.2015
comment
Re: XCTestException --- кодовая дислексия снова поражает! ржу не могу   -  person Brett Elliot    schedule 08.04.2015
comment
Можно ли поместить код настройки базы данных в вспомогательный метод? Тогда у вас будет только одна дублированная строка на тест.   -  person Joe Masilotti    schedule 30.01.2016


Ответы (3)


Есть два метода выполнения асинхронных тестов. XCTestExpectation и семафоры. В случае выполнения чего-либо асинхронного в setUp, вы должны использовать технику семафоров:

override func setUp() {
    super.setUp()

    // Fill out a database with data. I can make this call do anything, here
    // it returns a block.

    let data = getData()

    let semaphore = DispatchSemaphore(value: 0)

    db.overwriteDatabase(data) {

        // do some stuff

        semaphore.signal()
    }

    semaphore.wait()
}

Обратите внимание: для того, чтобы это работало, этот onDone блок не может работать в основном потоке (иначе вы попадете в тупик).


Если этот onDone блок выполняется в основной очереди, вы можете использовать циклы выполнения:

override func setUp() {
    super.setUp()

    var finished = false

    // Fill out a database with data. I can make this call do anything, here
    // it returns a block.

    let data = getData()

    db.overwriteDatabase(data) {

        // do some stuff

        finished = true
    }

    while !finished {
        RunLoop.current.run(mode: .default, before: Date.distantFuture)
    }
}

Это очень неэффективный шаблон, но в зависимости от того, как overwriteDatabase был реализован, может потребоваться

Обратите внимание: используйте этот шаблон только в том случае, если вы знаете, что блок onDone выполняется в основном потоке (в противном случае вам придется выполнить некоторую синхронизацию переменной finished).

person Rob    schedule 08.04.2015
comment
Есть ли способ заставить эту работу работать, если блок onDone работает в основном потоке? - person Brett Elliot; 08.04.2015
comment
Я обновил это примером, в котором используются циклы выполнения, если onDone выполняется в основном потоке. - person Rob; 08.04.2015
comment
Есть ли способ выполнить асинхронную однократную настройку внутри класса переопределения func setUp () {}, который должен вызываться только один раз для всех тестовых случаев? - person Ilker Baltaci; 03.04.2020
comment
@IlkerBaltaci - я бы подумал, что вы просто сделаете это в class воспроизведении. См. Общие сведения о настройке и завершении тестирования методов тестирования. - person Rob; 03.04.2020

Вместо использования семафоров или блокирующих циклов вы можете использовать ту же функцию waitForExpectationsWithTimeout:handler:, которую вы используете в своих тестовых примерах async.

// Swift
override func setUp() {
    super.setUp()

    let exp = expectation(description: "\(#function)\(#line)")

    // Issue an async request
    let data = getData()
    db.overwriteDatabase(data) {
        // do some stuff
        exp.fulfill()
    }

    // Wait for the async request to complete
    waitForExpectations(timeout: 40, handler: nil)
}

// Objective-C
- (void)setUp {
    [super setUp];

    NSString *description = [NSString stringWithFormat:@"%s%d", __FUNCTION__, __LINE__];
    XCTestExpectation *exp = [self expectationWithDescription:description];

    // Issue an async request
    NSData *data = [self getData];
    [db overwriteDatabaseData: data block: ^(){
        [exp fulfill];
    }];        

    // Wait for the async request to complete
    [self waitForExpectationsWithTimeout:40 handler: nil];
}
person RndmTsk    schedule 13.04.2016
comment
Здорово! Это то, что рекомендует Apple - ищите XCTestExpectation в справочнике Xcode API. Хотел бы я удвоить количество голосов за включение ObjC и Swift! - person David H; 12.08.2016
comment
Это должен быть принятый ответ. Есть отличный пост от NSHipster, в котором объясняется, как работать с асинхронным тестированием nshipster.com/xctestcase. - person isanjosgon; 12.10.2016
comment
В качестве побочного примечания, это не работает, когда вы используете функцию переопределения класса func setup () - person Mizmor; 25.04.2017

Swift 4.2

используйте это расширение:

import XCTest

extension XCTestCase {
    func wait(interval: TimeInterval = 0.1 , completion: @escaping (() -> Void)) {
        let exp = expectation(description: "")
        DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
            completion()
            exp.fulfill()
        }
        waitForExpectations(timeout: interval + 0.1) // add 0.1 for sure asyn after called
    }
}

и использование вроде этого:

func testShoudDeleteSection() {
        let tableView = TableViewSpy()
        sut.tableView = tableView

        sut.sectionDidDelete(at: 0)

        wait {
            XCTAssert(tableView.isReloadDataCalled, "Chcek relaod table view after section delete")
        }
    }

приведенный выше пример не является полным, но вы можете понять идею. надеюсь, что это поможет.

person moraei    schedule 24.02.2019