Состояние гонки в юнит-тестах

В настоящее время я тестирую ряд классов, которые выполняют сетевые операции, такие как вызовы REST API, и база данных Realm мутирует в процессе. Когда я запускаю все различные тесты, которые у меня есть, появляются условия гонки (но, конечно, когда я запускаю их один за другим, они все проходят). Как я могу надежно пройти тестирование?

Я попытался вызвать упомянутые функции в блоке GCD следующим образом:

DispatchQueue.main.async {
    self.function.start()
}

Один из моих тестов все еще не пройден, поэтому я думаю, что вышеописанное не сработало. Я включил Thread Sanitizer, и он время от времени сообщает, что появляются условия гонки.

Я не могу опубликовать код, поэтому я ищу концептуальные решения.

1 ответ

Как правило, некоторая форма внедрения зависимости. Будь то внутренне представленная переменная для DispatchQueue, аргумент по умолчанию в функции с очередью или аргумент конструктора. Вам просто нужен какой-то способ пройти тестовую очередь, которая отправляет событие, когда вам нужно.

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

Пример (заявление об отказе: я печатаю по памяти, чтобы он не компилировался, но он дает идею):

// In test code.
struct TestQueue: DispatchQueue {

    // make sure to impement other necessary protocol methods

    func async(block: () -> Void) {
        // you can even have some different behavior for when to execute the block. 
        // also you can pass XCTestExpectations to this TestQueue to be fulfilled if necessary.
        block()
    }
}

// In source code. In test, pass the Test Queue to the first argument
func doSomething(queue: DispatchQueue = DispatchQueue.main, completion: () -> Void) {
    queue.async(block: completion)
}

Другие методы тестирования асинхронности и устранения условий гонки связаны с искусным выполнением XCTestExpectation.

Если у вас есть доступ к блоку завершения, который в конечном итоге вызывается:

// In source 
class Subject {

    func doSomethingAsync(completion: () -> Void) {
        ...
    }

}

// In test

func testDoSomethingAsync() {
    let subject = Subject()
    let expect = expectation(description: "does something asnyc")
    subject.doSomethingAsync {
        expect.fulfill()
    }

    wait(for: [expect], timeout: 1.0)
    // assert something here
    // or the wait may be good enough as it will fail if not fulfilled
}

Если у вас нет доступа к блоку завершения, обычно это означает, что нужно найти способ внедрить или создать подкласс тестового двойника, для которого вы можете установить XCTestExpectation, и в конечном итоге он будет соответствовать ожиданиям после завершения асинхронной работы.

Другие вопросы по тегам