Всем привет! Меня зовут Андрей, я Go-разработчик. Сегодня хочу поделиться библиотекой, которая родилась из внутренней боли и желания оптимизировать рабочий процесс.
Проблема: «Ну сколько можно ждать?»
Классический сценарий подготовки базы для интеграционного теста выглядит так:
func TestMyService(t *testing.T) {
// 1. Создать новую БД (CREATE DATABASE)
// 2. Применить все миграции (N запросов CREATE TABLE, INDEX, FK...)
// 3. Запустить сам тест
// 4. Удалить БД (DROP DATABASE)
// ... и так для КАЖДОГО теста.
}
Шаги 1 и 2 повторяются каждый раз, съедая кучу времени. Чем сложнее ваша схема (таблицы, индексы, внешние ключи), тем дольше длится этот процесс.
Решение: Шаблоны (Templates) PostgreSQL
В PostgreSQL есть мощная, но не всегда очевидная фича — шаблонные базы данных (Template Databases). Вы можете создать одну «шаблонную» базу, применить все миграции единожды и сделать ее шаблоном. Все последующие базы создаются командой:
CREATE DATABASE my_fast_test_db TEMPLATE my_template_db;
Эта операция копирует данные на уровне файловой системы и занимает мгновение, независимо от сложности схемы.
Моя библиотека pgdbtemplate
автоматизирует всю эту магию, предоставляя простой и удобный API для ваших тестов.
Начинаем работать за 5 минут
Установка стандартная:
go get github.com/andrei-polukhin/pgdbtemplate
А вот так это выглядит в коде ваших тестов:
package main
import (
"context"
"fmt"
"log"
"github.com/andrei-polukhin/pgdbtemplate"
"github.com/andrei-polukhin/pgdbtemplate-pgx"
)
func main() {
// Create a connection provider with pooling options.
connStringFunc := func(dbName string) string {
return fmt.Sprintf("postgres://user:pass@localhost/%s", dbName)
}
provider := pgdbtemplatepgx.NewConnectionProvider(connStringFunc)
// Create migration runner.
migrationRunner := pgdbtemplate.NewFileMigrationRunner(
[]string{"./migrations"},
pgdbtemplate.AlphabeticalMigrationFilesSorting,
)
// Create template manager.
config := pgdbtemplate.Config{
ConnectionProvider: provider,
MigrationRunner: migrationRunner,
}
tm, err := pgdbtemplate.NewTemplateManager(config)
if err != nil {
log.Fatal(err)
}
// Initialize template with migrations.
ctx := context.Background()
if err := tm.Initialize(ctx); err != nil {
log.Fatal(err)
}
// Create test database (fast!).
testDB, testDBName, err := tm.CreateTestDatabase(ctx)
if err != nil {
log.Fatal(err)
}
defer testDB.Close()
defer tm.DropTestDatabase(ctx, testDBName)
// Use testDB for testing...
log.Printf("Test database %s ready!", testDBName)
}
Цифры говорят сами за себя
Я провел детальные бенчмарки, сравнивая традиционный подход и подход с шаблонами. Результаты впечатляют:
? Сравнение скорости (меньше — лучше)
Сложность схемы |
Классический подход |
Через шаблоны |
Ускорение |
---|---|---|---|
1 таблица |
28.9 мс |
28.2 мс |
1.03x |
3 таблицы |
39.5 мс |
27.6 мс |
1.43x |
5 таблиц (+индексы) |
43.1 мс |
28.8 мс |
1.50x |
? Массовое создание баз
Количество баз |
Классический подход |
Через шаблоны |
Экономия времени |
---|---|---|---|
20 баз |
906.8 мс |
613.8 мс |
32% |
50 баз |
2.29 с |
1.53 с |
33% |
200 баз |
9.21 с |
5.84 с |
37% |
500 баз |
22.31 с |
14.82 с |
34% |
Главный вывод: скорость подхода с шаблонами не зависит от сложности схемы. Пока классический метод будет всё больше замедляться с ростом числа таблиц и индексов, метод с шаблонами остается стабильно быстрым.
Что под капотом?
Инициализация: Создается база-шаблон, на нее один раз накатываются все миграции.
Тестирование: Для каждого теста создается новая база через
CREATE DATABASE ... TEMPLATE
— это быстрое копирование на уровне файловой системы PostgreSQL.Очистка: После всех тестов удаляются все созданные тестовые базы и сам шаблон.
Для кого этот инструмент?
У Вас больше 10 тестов, связанных с базой данных.
Ваша схема данных сложнее 2-3 таблиц.
Вы часто запускаете тесты во время разработки.
Ваш CI-пайплайн включает этап с интеграционными тестами БД.
Вы цените свое время и не хотите ждать лишние 10 секунд при каждом запуске.
Полезные ссылки
GitHub репозиторий: github.com/andrei-polukhin/pgdbtemplate
Документация (ENG): pkg.go.dev
Буду рад вашим звёздочкам на GitHub, пул-реквестам и issue! Что думаете о таком подходе? Сталкивались ли с подобной проблемой и как решали её раньше?
Большое спасибо за прочтение поста!
Комментарии (0)
xcono
17.09.2025 08:00Также можно взглянуть на https://github.com/peterldowns/pgtestdb - проекты очень похожи.
andrei-polukhin Автор
17.09.2025 08:00Спасибо за комментарий. Соглашусь с вами и, если честно, я узнал об этом проекте уже из коммьюнити на Reddit :) Я посмотрел эту библиотеку и нашел ее очень хорошей. В то же время, два проекта имеют слегка разный фокус и я скопирую вам, что написал на Reddit'е на английском. Дайте знать, если русский вам будет удобнее английского.
To the differences:
pgtestdb
allows the user to call theNew
function and not worry about creating or cleaning up the test database — an out-of-the-box solution. Both libraries supportpq
andpgx
; perhapspdbtemplate
allows making it more explicit via the definedNewStandardConnectionProvider
andNewPgxConnectionProvider
. Yet the focus of libraries is different: I've already shared my ideas on the one you've shared, while the most significant benefit ofpgdbtemplate
is flexibility and control:Creating a custom connection provider by implementing
pgdbtemplate.ConnectionProvider
Controlling the lifecycle of the databases and how they are created & dropped in the tests
Stricter approach to the predefined connectors pq and pgx (both libraries import it, but it is done statically and not via
DriverName
in pgdbtemplate)Ability to either use the predefined
FileMigrationRunner
orNoOpMigrationRunner
(present in both, butpgtestdb
has support for migration plugins, while pgtestdb does not). You can read more about advanced use cases in this document: https://github.com/andrei-polukhin/pgdbtemplate/blob/main/docs/ADVANCED.mdClear benchmarks: specified only in
pgdbtemplate
and notpgtestdb
.
I think the ultimate choice is between wanting to import and go (pgtestdb) — and well-defined abstractions to give the end customer as much control and flexibility as possible (pgdbtemplate). This was my conclusion, but I can be biased, as I am the author of pgdbtemplate ;)
What was your take on the differences between the libraries? If you'd like to share some suggestions for the projects, I'd love to hear more.
OlegIct
17.09.2025 08:00можно ли указать STRATEGY для создания базы?
andrei-polukhin Автор
17.09.2025 08:00Спасибо за комментарий, это очень технически грамотный вопрос. Нет, указывать
STRATEGY
дляCREATE DATABASE ... TEMPLATE
не нужно, и в большинстве случаев это даже невозможно. Эта опция не имеет отношения к механизму шаблонов, описанному в этой статье.STRATEGY
- это опция, которая управляет методом копирования при создании обычной базы данных из другой обычной базы данных (не шаблона). Это не имеет абсолютно никакого отношения к созданию базы данных из шаблона. Попытка добавитьSTRATEGY
к команде сTEMPLATE
вызовет ошибку, так как эти два пункта синтаксически взаимоисключающие. Postgres сам оптимизирует все нужное при создании БД из template'а и дополнительный параметр указывать не нужно.С уважением,
АндрейOlegIct
17.09.2025 08:00попытка добавить не вызывает ошибки:
postgres=# create database db1 strategy file_copy template template1; CREATE DATABASE
Вероятно, вы использовали GPT, а он пишет ложь, не стоит его использовать.
Свойство "шаблон" в PostgreSQL даёт только то, что без снятия свойства нельзя удалить базу.
Вопрос был в том, что вместо команд SQL для создания базы используются программные вызовы и есть ли в программных вызовах STRATEGY. Я столкнулся с тем, что в pgx нет возможности передать PGOPTIONS.
По скорости, думаю, то же самое - не может программный вызов быть быстрее команды SQL. Основное что влияет на скорость создания баз это её размер и strategy.
IvanG
17.09.2025 08:00Спасибо за вклад в развитие авто тестов, но надеюсь никто не действует сейчас как вы описали - создать базу с нуля и накатывать миграции для каждого рана теста.
У себя на проектах уже лет 10 использую создание бд/бэкапирование и разворачивание бэкапа на каждый ран (придумал не я, еще зеленым увидел), все это мс скл, может уже тоже какие нить темплейты заехали, но старого коня не поменяли ещё ))
andrei-polukhin Автор
17.09.2025 08:00Спасибо за конструктивный и добрый комментарий :) Мне интересно, как сейчас делаются такие вещи в разных компаниях — были бы вы открыты рассказать, какими библиотеками/инструментами пользуетесь?
anaxita
Я что то так и не понял чем это решение отличается от вызова create database from template и drop database в конце?
andrei-polukhin Автор
Спасибо за комментарий. Я полагаю, такой вопрос можно поставить ко всем абстракциям / декомпозициям. Разумеется, можно делать все это мануально, однако библиотека делает это безопасно с точки зрения асинхронного программирования, дает четкий интерфейс абстракций (что делать за чем), а также имеет четкие benchmark'и.
К примеру, есть очень edge cases, которые трудно делать мануальными командами - что если template база данных создалась, но подключиться к ней не удается? Ее нужно удалить, а еще сказать пользователю о всех ошибках на этом этапе. Вот примеры: https://github.com/andrei-polukhin/pgdbtemplate/blob/89baa761f6d79ed92fc7c6db77ed60f76a230f32/template_manager.go#L183-L202 и https://github.com/andrei-polukhin/pgdbtemplate/blob/89baa761f6d79ed92fc7c6db77ed60f76a230f32/template_manager.go#L316-L329.
Кстати, создать вот такой Cleanup метод, который раз и удалит все базы данных, было бы очень трудно в более "кустарных" условиях, так как нужно держать в голове thread-safe способ keep track of этих баз данных. Надеюсь, я объяснил понятно.
Если же вам интересны абстракции в целом, то я сейчас планирую делать разбивку библиотеки, чтобы вынести pq и pgx драйверы из нее... было ли бы вам интересно обсудить эти абстракции в личных сообщениях? Мне важно мнение community.
С уважением,
Андрей