Go: GORM
Теория: Тестирование кода с GORM
Работа с базой данных всегда добавляет тестам сложности. Нужно, чтобы код ходил в «настоящую» БД, проверял связи, ограничения, ошибки миграций — и при этом тесты запускались быстро, не ломали друг друга и не требовали отдельного PostgreSQL на машине каждого разработчика.
С GORM удобно выстроить три слоя:
- поднимать лёгкую тестовую базу в памяти (обычно SQLite);
- изолировать каждый тест транзакцией и откатом;
- работать с фиксированным набором тестовых данных (фикстурами) и уметь быстро очищать базу.
Разберём, как это сделать в Go-проекте с GORM.
SQLite в памяти как тестовая база
Для юнит-уровня нам не нужна вся мощь продакшн-PostgreSQL. Гораздо удобнее использовать SQLite в памяти. Она живёт в процессе теста, поднимается за миллисекунды, не требует установки и отлично поддерживается GORM.
Идея такая: на старте тестов открываем in-memory SQLite, включаем проверку внешних ключей, один раз прогоняем миграции — и дальше используем это подключение во всех тестах.
Пример вспомогательной функции:
Здесь важны несколько деталей.
Во-первых, строка подключения file::memory:?cache=shared. Это не просто «какая-то память», а общий in-memory стор для всех соединений внутри процесса. GORM использует пул соединений, и без ?cache=shared данные могли бы теряться между разными коннектами.
Во-вторых, PRAGMA foreign_keys = ON. Без него SQLite никак не реагирует на нарушения внешних ключей. Для нас это критично: тесты должны вести себя так же строго, как и продакшн-база. Если нельзя создать пост без пользователя, тест обязан упасть.
В-третьих, миграции выполняются один раз в newTestDB. Не нужно дёргать AutoMigrate() в каждом тесте — это медленно и создаёт гонки, если тесты идут параллельно.
Изоляция тестов транзакциями
Даже с быстрой базой в памяти тесты легко начать «мусорить» данными: один тест создал пользователя, другой тест внезапно его нашёл и прошёл, хотя по смыслу должен был работать с пустой базой.
Один простой приём решает проблему: оборачиваем тело каждого теста в транзакцию и откатываем её в конце. Всё, что произошло внутри, исчезает, как только тест завершился.
Вводим небольшой хелпер:
Теперь сам тест работает только через tx:
После завершения withTx() вызывается Rollback(), и созданные записи исчезают. Можно проверить это прямо в тесте:
Если всё сделано правильно, счётчик после транзакции будет нулевой.
Ключевой момент — дисциплина: всё, что касается базы в тесте и внутри тестируемых функций, должно работать через *gorm.DB, полученный из withTx(). Если где-то в глубине кода вы возьмёте глобальный db и сделаете db.Create(...), эта запись окажется вне транзакции и никуда не денется при Rollback().
Фикстуры и очистка базы
Тесты редко ограничиваются одной вставкой и выборкой. Чаще нужно сложное стартовое состояние: несколько пользователей, посты, связи, справочники. При этом состояние должно быть:
- предсказуемым;
- легко читаемым;
- быстро подготавливаемым.
Здесь помогают фикстуры — заранее описанные наборы данных. Их можно задавать Go-кодом (фабрики), JSON/YAML-файлами или сырими SQL-скриптами.
В связке с транзакциями схема обычно такая: тест открывает транзакцию, заливает фикстуры, прогоняет проверки, делает Rollback(). База возвращается к нулевому состоянию, соседние тесты никак не зависят от этого набора данных.
Примитивные фабрики:
Функция, которая засевает минимальный набор:
Тест с такими фикстурами выглядит понятно:
Все данные живут только внутри транзакции. После Rollback() база снова чистая.
Иногда транзакций недостаточно. Например, вы гоняете интеграционные тесты поверх реального PostgreSQL, у вас крутятся фоновые воркеры, а приложение само открывает транзакции внутри. В таком случае удобнее явная очистка: перед тестовым набором или перед каждым тестом очищать таблицы и сбрасывать последовательности.
Простейший пример для SQLite:
Для PostgreSQL часто используют TRUNCATE ... RESTART IDENTITY CASCADE для нескольких таблиц сразу. В любом случае принцип один: перед тестом очистили, засеяли фикстуры, прогнали сценарий, при необходимости очистили ещё раз.
На что обращать внимание
Тестовая база с GORM хорошо работает, если помнить несколько правил.
Во-первых, не перенагружать AutoMigrate(). Поднять схему один раз при создании *gorm.DB и полагаться на транзакции и фикстуры. Запускать миграции в каждом тесте — простой способ замедлить проект и получить гонки.
Во-вторых, везде использовать tx, где нужна изоляция. Любой вызов через «голый» db внутри теста обходит транзакцию и сохраняет данные навсегда.
В-третьих, следить за внешними ключами в фикстурах. С включёнными PRAGMA foreign_keys = ON (или в PostgreSQL по умолчанию) порядок вставки важен: сначала «родители», потом «дети» и таблицы связей. Попытка создать пост с UserID, которого ещё нет, должна падать, и тест должен это увидеть.



