Всем привет! Меня зовут Андрей, я 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%

Главный вывод: скорость подхода с шаблонами не зависит от сложности схемы. Пока классический метод будет всё больше замедляться с ростом числа таблиц и индексов, метод с шаблонами остается стабильно быстрым.

Что под капотом?

  1. Инициализация: Создается база-шаблон, на нее один раз накатываются все миграции.

  2. Тестирование: Для каждого теста создается новая база через CREATE DATABASE ... TEMPLATE — это быстрое копирование на уровне файловой системы PostgreSQL.

  3. Очистка: После всех тестов удаляются все созданные тестовые базы и сам шаблон.

Для кого этот инструмент?

  • У Вас больше 10 тестов, связанных с базой данных.

  • Ваша схема данных сложнее 2-3 таблиц.

  • Вы часто запускаете тесты во время разработки.

  • Ваш CI-пайплайн включает этап с интеграционными тестами БД.

  • Вы цените свое время и не хотите ждать лишние 10 секунд при каждом запуске.

Полезные ссылки

Буду рад вашим звёздочкам на GitHub, пул-реквестам и issue! Что думаете о таком подходе? Сталкивались ли с подобной проблемой и как решали её раньше?

Большое спасибо за прочтение поста!