Код, который зависит от времени, невозможно проверить в лоб. Поле поиска не шлёт запрос на каждое нажатие - оно ждёт паузы в 300 миллисекунд и только потом отправляет последнее значение. Это debounce. Токен доступа живёт час и потом истекает. Во всех этих случаях поведение определяется не входом, а ходом времени, и обычный тест либо не дожидается нужного момента, либо ловит случайный результат.
Наивный ответ - подождать по-настоящему: поставить в тесте setTimeout на 300 мс и проверить результат после. Это естественно, ведь так работает продакшен. Но реальное ожидание превращает быстрый юнит-тест в медленный, а сумма таких пауз растягивает прогон на минуты. Хуже того, тест становится flaky: на нагруженном CI-раннере таймер срабатывает позже, и проверка, рассчитанная впритык, случайно падает.
Механизм, который убирает эту неопределённость, - поддельные таймеры. Вызов jest.useFakeTimers подменяет глобальные setTimeout, setInterval и Date управляемыми двойниками из библиотеки @sinonjs/fake-timers, так что время больше не идёт само - вы двигаете его вручную. Современная реализация стала стандартной ещё в Jest 27 и остаётся такой в Jest 30, где библиотеку обновили и добавили advanceTimersToNextFrame для кадров анимации. Включать её отдельной опцией не нужно.
Дальше вы управляете часами явно. jest.advanceTimersByTime(300) сдвигает поддельные часы на 300 мс вперёд и синхронно запускает все таймеры, чей срок настал. В тесте debounce это ключевой момент: вы дважды вызываете обёрнутую функцию, прокручиваете время на 300 мс и убеждаетесь, что настоящий поиск ушёл ровно один раз и с последним значением. Никаких реальных пауз - тест мгновенный и одинаковый на любой машине.
Поддельные часы - это глобальная подмена, поэтому её обязательно откатывать. useFakeTimers в beforeEach и useRealTimers в afterEach гарантируют, что следующий тест начнётся с настоящим временем, а не с чужими таймерами. Если нужно зафиксировать конкретный момент - например, проверить, что токен, выданный в 10:00, истекает в 11:00, - jest.setSystemTime задаёт точную дату; эта функция есть только в современной реализации и недоступна в устаревшей.
Тут стоит провести границу. Поддельные таймеры хороши для поведения планировщика - debounce, throttle, опрос, ретраи. Но когда доменная логика сама спрашивает, какое сегодня число, подменять для этого глобальные часы - грубо. Чище внедрить Clock: маленькую зависимость с методом now(), которую в проде реализует система, а в тесте - функция, возвращающая нужную дату. Так дата становится обычным входом, а не скрытым глобальным состоянием.
Особая осторожность нужна с рекурсивными таймерами. Опрос, который в конце каждого шага заводит следующий setTimeout, никогда не закончится, и вызов jest.runAllTimers прокрутит бесконечный цикл до зависания теста. Для таких случаев двигайте время дозированно через advanceTimersByTime или запускайте только уже стоящие в очереди таймеры через runOnlyPendingTimers.
Цена у механизма есть. Забытый useRealTimers отравляет соседние тесты; смесь таймеров и промисов требует аккуратности - сдвинуть часы, а затем дождаться микрозадач через await. Зато отказ от поддельных таймеров даёт классический flaky: тест с реальным ожиданием в 300 мс проходит на быстрой машине разработчика и падает по таймауту на медленном CI. Детерминированные часы превращают время из источника случайности в ещё один управляемый вход теста.
beforeEach(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2026-07-26T10:00:00Z'))
})
afterEach(() => jest.useRealTimers())
test('отправляет поиск после 300 ms debounce', async () => {
const search = jest.fn()
const debounced = debounce(search, 300)
debounced('je')
debounced('jest')
jest.advanceTimersByTime(300)
expect(search).toHaveBeenCalledTimes(1)
expect(search).toHaveBeenCalledWith('jest')
})