Go: Автоматическое тестирование
Теория: Параллельные тесты и t.Parallel
В обычных тестах в Go каждая функция выполняется последовательно. Это просто и безопасно, но иногда долго. Если тестов сотни или тысячи, общее время прогонки может быть значительным. В Go есть инструмент для ускорения — параллельные тесты. С помощью метода t.Parallel() тесты можно запускать одновременно.
Но параллельность — это палка о двух концах. С одной стороны, тесты проходят быстрее. С другой стороны, если код работает с общими данными, параллельное выполнение может привести к неожиданным результатам. Такие ситуации называются гонками данных.
Что такое гонки данных
Гонка данных (race condition) — это ситуация, когда несколько потоков (или горутин) одновременно читают и изменяют одну и ту же переменную, и результат зависит от порядка выполнения.
Проще всего это понять на примере.
Функция увеличивает глобальный счётчик. Если запустить её из нескольких горутин одновременно, получится хаос. Иногда результат будет верный, иногда — меньше, чем ожидалось.
Исторически такие ошибки — одни из самых сложных в отладке. В 70–80-е годы программисты писали многопоточные системы без хороших инструментов, и гонки данных приводили к самым странным сбоям: от неправильных расчётов до падений ядерных симуляторов. Ошибка могла проявляться раз в миллион запусков — именно поэтому такие баги страшны.
Как Go помогает находить гонки
Go имеет встроенный инструмент для поиска гонок. Достаточно запустить тесты с флагом -race:
Go запускает код под особым режимом: отслеживает доступы к памяти и проверяет, не изменяют ли разные горутины одну и ту же переменную без синхронизации. Если проблема есть, Go выведет предупреждение.
Пример:
Это сообщение указывает: была гонка данных в функции Increment. Такой инструмент сильно упрощает жизнь — баги находят тесты, а не пользователи.
Первый пример: последовательные тесты
Для функции без побочных эффектов параллельность безопасна.
Тесты без параллельности:
Тесты проходят последовательно. Всё верно, но долго, если таких тестов сотни.
Подключение параллельности
Чтобы тест можно было запускать одновременно с другими, в его начале вызывают t.Parallel().
Теперь тесты могут выполняться одновременно. Так экономится время.
Параллельные табличные тесты
Особое внимание нужно уделять табличным тестам. В них часто используется цикл, и есть риск допустить ошибку с замыканиями.
Ошибочный код:
Здесь переменная c одна на весь цикл, и все под-тесты начинают драться за неё. Результаты будут случайными.
Правильный вариант: делать копию c внутри цикла.
Теперь каждый под-тест работает со своим набором данных.
Как защититься от гонок
Есть несколько стратегий.
Первое: стараться делать тесты независимыми. У каждого теста — свои данные и своя временная директория. Для файлов лучше использовать t.TempDir(), чтобы тесты не затирали друг другу результаты.
Второе: если тесты работают с общим ресурсом, его нужно защищать. Для этого есть мьютекс.
Что такое мьютекс
Мьютекс (от англ. mutual exclusion, «взаимное исключение») — это объект, который разрешает доступ к ресурсу только одному потоку одновременно. Пока один поток держит мьютекс, другие ждут.
В Go это реализуется через sync.Mutex.
Пример с небезопасным счётчиком:
Здесь параллельные вызовы приведут к гонке.
Исправленный вариант с мьютексом:
Теперь даже если 100 горутин вызовут IncrementSafe одновременно, результат будет корректным: каждая по очереди возьмёт замок, увеличит счётчик и освободит ресурс.
Когда параллельность не нужна
Если тесты используют глобальные переменные, внешний ресурс (например, файл или базу данных), или порядок выполнения критичен, лучше не включать t.Parallel(). В таких случаях безопаснее оставить последовательный запуск.
t.Parallel() позволяет ускорить выполнение тестов, но пользоваться им нужно осторожно. Как только в игру вступает параллельность, появляется риск гонок данных — ситуаций, когда несколько горутин одновременно обращаются к одной переменной и меняют её без синхронизации.
Go помогает разработчику: при запуске с флагом -race он автоматически отслеживает доступ к памяти и сообщает о проблемах. Но устранить их обязан сам программист. Чаще всего это делается так: тесты стараются строить так, чтобы каждый работал только со своими данными, для временных файлов применяют t.TempDir(), а при необходимости разделять общий ресурс используют мьютексы.
Мьютекс можно представить как замок на двери: пока один поток держит ключ и работает с переменной, остальные ждут своей очереди. Благодаря этому состояние данных всегда остаётся согласованным.
В итоге параллельные тесты превращаются из потенциальной угрозы в полезный инструмент. Они позволяют делать проверки быстрее и одновременно учат код «жить» в условиях конкуренции, что особенно важно для программ, которые должны быть устойчивыми и надёжными в реальной многопоточной среде.



