Протестируйте зависящий от времени код (debounce) без Thread.sleep, внедрив clock
Debouncer.run(_:) планирует действие после фиксированной задержки через DispatchQueue.main.asyncAfter, поэтому тест вынужден ждать реальное время. Отрефакторьте его так, чтобы он принимал внедрённый планировщик (или clock) и тест мог детерминированно прокручивать время без Thread.sleep.
Требования:
- Внедрите зависимость времени; по умолчанию берите реальный планировщик в проде.
- Тест должен прокручивать виртуальное время, а не ждать реальные секунды.
final class Debouncer {
private let delay: TimeInterval
init(delay: TimeInterval) { self.delay = delay }
func run(_ action: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { action() }
}
}
Допишите реализацию.
Сделайте время зависимостью, а не глобальным вызовом. Внедрите абстракцию — протокол Scheduler или Clock — и планируйте через неё, по умолчанию беря реальную в проде. Тест передаёт виртуальный планировщик и прокручивает его без Thread.sleep.
- ✗Ждут реальное время через
Thread.sleepвместо виртуального - ✗Прикрывают недетерминированное время таймаутом ожидания
- ✗Зовут таймеры
DispatchQueueнапрямую, а не через внедрённый планировщик
- →Как виртуальный планировщик прокручивает время без реального ожидания?
- →Почему обнуление задержки меняет поведение, которое вы хотели проверить?
Inject an abstraction over time; production gets the real scheduler, the test gets a virtual one.
final class Debouncer<S: Scheduler> {
private let delay: S.SchedulerTimeType.Stride
private let scheduler: S
private var token: AnyCancellable?
init(delay: S.SchedulerTimeType.Stride,
scheduler: S = DispatchQueue.main) {
self.delay = delay
self.scheduler = scheduler
}
func run(_ action: @escaping () -> Void) {
token = Just(())
.delay(for: delay, scheduler: scheduler)
.sink { _ in action() }
}
}
// Test — advance a virtual scheduler instead of sleeping:
func testDebounceFiresAfterDelay() {
let scheduler = DispatchQueue.test // e.g. combine-schedulers' TestScheduler
let debouncer = Debouncer(delay: .seconds(1), scheduler: scheduler)
var fired = 0
debouncer.run { fired += 1 }
scheduler.advance(by: .milliseconds(999))
XCTAssertEqual(fired, 0)
scheduler.advance(by: .milliseconds(1))
XCTAssertEqual(fired, 1)
}
Because the debouncer schedules through the injected Scheduler, production still uses DispatchQueue.main, while the test drives a TestScheduler and advance(by:) moves virtual time forward instantly. No Thread.sleep, no timeout — the test is fully deterministic. Swift's Clock protocol supports the same pattern for async code.