Состояние гонки в юнит-тестах
В настоящее время я тестирую ряд классов, которые выполняют сетевые операции, такие как вызовы 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, и в конечном итоге он будет соответствовать ожиданиям после завершения асинхронной работы.