TGViewer
Go Update Go Update @go_update · 3.22K subscribers
Post #90 2.25K
⏲️ testing/synctest — останавливаем время ⏲️

Одними из самых сложных сценариев для тестирования являются сценарии где замешано время. Если использовать реальное время то тесты либо а) начинают занимать слишком много времени либо б) теряют в надежности из-за гонки "кода со временем". Особенно хорошо это ощущается в тестах которые используют time.Ticker, time.Timer и time.AfterFunc для управление потоком исполнения.

Приведу очень простой пример:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

for {
select {
case <-time.After(500 * time.Millisecond):
println("do work...")
case <-ctx.Done():
println("Context done:", ctx.Err())
}
}


Для того, что-бы убедиться, что наш код выведет "do work..." 4 раза нам нужно подождать примерно 2 секунды, в течении которых наша программа фактически ничего не делает. Более того, нет никаких гарантий, что наш текст будет выведен именно 4 раза, а не 3 или 2 с учетом нагрузки на планировщик ОС.

Когда такой тест у нас один это нормально. Когда у нас их десятки это проблема. А когда у нас их за сотню то это минуты простаивающего CI, который мог бы делать что-то полезное. И тут у нас есть два решения:

• В пределах тестов кратно уменьшить время "ожидания". Допустим не 500мс а 50. Или вообще 5мс. Проблема такого подхода в том, что исполнение (и работа планировщика) тоже занимает время и тест становится менее надежным. При этом проблема "впустую потраченного времени" никуда не уходит.
• Используем одну из сторонних библиотек для "подделки" времени и замыкаемся на ее семантику (и баги) по всей программе.

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

Хорошая новость, что разработчики языка озаботились это проблемой, и в Go 1.25 пакет synctest стал наконец доступен любому желающему. Публичное API содержит всего две функции:

Первая это synctest.Test. Она запускает замыкание в изолированном пространстве-пузыре где время течёт "иначе". Все горутины созданные внутри этого замыкания разделяют этот "пузырь". Время в них стоит на месте, до тех пор пока все горутины не станут "прочно заблокированы". Термин "прочно заблокированы" может прозвучать странно, но по сути это лишь определенный набор блокировок которые приводят к сдвигу времени в "пузыре». Вот их полный список:

• Блокирующая попытка посылки или получения данных из канала созданного в том же "пузыре".
• select с блокирующими операциями над каналами созданным в том же "пузыре".
• Вызов sync.Cond.Wait или time.Sleep.
• sync.WaitGroup.Wait, если sync.WaitGroup.Add был вызван из того же "пузыря" (не горутины, это важно).

Однако следующие операции не приводят к "прочной блокировке" горутин:
• Лок мьютексов.
• Блокировка на операциях ввода-вывода (файлы, сеть и прочая).
• Сисколлы.

Вторая функция это synctest.Wait(). Ее единственная роль — дождаться момента когда все горутины в "пузыре" достигли "прочной блокировки" и затем вернуться.

Таком образом как только все горутины "прочно заблокированы" происходит одно из следующих:
• Если Wait был вызван, то он возвращается. Время не сдвигается.
• В противном случае время внутри "пузыря" сдвигается на достаточную величину для запуска в работу хотя-бы одной горутины если функция создававшая "пузырь" не закончила исполнение.
• В противном случае, происходит дедлок и наш вызов Test паникует.

Проще всего понять на живом примере (код не влез в этот пост). Обратите внимание, что время выставлено в часы однако время теста и порядок вывода в лог сохраняется раз за разом. Так мы обеспечиваем не только быстрое исполнение, но и ожидаемый детерминизм. Другой плюс нового пакета, заключается в том, что если горутина создавшая "пузырь" выйдет до того, как закончат работу все её дочерние горутины, то вы получите панику (пример). Таким образом на этапе тестирования еще и обеспечивается контроль очистки ресурсов.

Для более подробной документации рекомендую глянуть документацию. Там-же приведены и другие примеры, сценарии и особенности работы.
go.dev Go Playground - The Go Programming Language
  • 👍 11
  • 🔥 3
More from @go_update
  1. Aug 22, 2026Об изоляции LLM Да, это ещё один пост про работу с Codex/Claude/GLM/Qwen/DeepSeek и прочая…
  2. Aug 11, 2026И вот эти два минуса выглядят нерешенными (на данный момент времени). Можно ли их решить в…
  3. Aug 11, 2026🎂 Вечерний пост о том, что сегодня мне исполнилось 34. Прошёл еще один год, а значит врем…
  4. Jul 13, 2026📝 testing: allow examples with any signature Небольшое «Quality of Life» предложение. Сут…
  5. May 14, 2026📝 net/http/httptest: synctest support Я уже писал про пакет synctest и его возможности. Э…
  6. May 13, 2026Я редко пишу сюда о вещах которые не относятся к Go, но тут у меня появилась хорошая стать…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →