Golang快速创建PostgreSQL测试数据库的工具pgdbtemplate

Golang快速创建PostgreSQL测试数据库的工具pgdbtemplate 厌倦了等待测试套件一遍又一遍地缓慢创建和迁移 PostgreSQL 数据库吗?如果你正在用 Go 编写数据密集型应用程序,你一定深知这种痛苦。你的测试花费在设置数据库上的时间比实际测试逻辑的时间还要多。

如果我告诉你,有一种方法可以让这个过程快 1.5 倍少用 17% 的内存,并且能轻松扩展到数百个测试数据库,你会怎么想?

来认识一下 pgdbtemplate —— 一个高性能的 Go 库,它利用 PostgreSQL 的原生模板数据库来彻底改变你的测试工作流程。

问题:传统的数据库设置很慢

集成测试的经典方法如下所示:

func TestUserService(t *testing.T) {
    // 1. 创建新数据库
    // 2. 运行所有迁移(CREATE TABLE, INDEX, FK...)
    // 3. 运行你的测试
    // 4. 删除数据库
}

步骤 1 和 2 会为每一个测试重复执行,消耗宝贵的时间和资源。你的模式越复杂,速度就越慢。

解决方案:PostgreSQL 模板数据库

PostgreSQL 有一个出色的内置功能:模板数据库。你可以创建一个应用了所有迁移的“黄金”数据库,然后将其用作模板,在几毫秒内创建新的、完全相同的数据库。

pgdbtemplate 自动化了这个过程,提供了一个简单的 API,可以无缝集成到你现有的测试设置中。

几分钟内快速上手

首先,安装这个库:

go get github.com/andrei-polukhin/pgdbtemplate

以下是如何在你的测试套件中使用它:

package main

import (
    "context"
    "testing"

    "github.com/andrei-polukhin/pgdbtemplate"
)

var templateManager *pgdbtemplate.TemplateManager

func TestMain(m *testing.M) {
    ctx := context.Background()
    
    // 一次性设置模板管理器
    connStringFunc := func(dbName string) string {
        return "postgres://user:pass@localhost/" + dbName
    }
    
    provider := pgdbtemplate.NewPgxConnectionProvider(connStringFunc)
    migrationRunner := pgdbtemplate.NewFileMigrationRunner(
        []string{"./migrations"},
        pgdbtemplate.AlphabeticalMigrationFilesSorting,
    )

    tm, _ := pgdbtemplate.NewTemplateManager(pgdbtemplate.Config{
        ConnectionProvider: provider,
        MigrationRunner:    migrationRunner,
    })
    
    // 使用迁移初始化模板(一次性操作)
    tm.Initialize(ctx)
    
    templateManager = tm
    code := m.Run()
    
    // 清理所有内容
    tm.Cleanup(ctx)
    os.Exit(code)
}

func TestUserRepository(t *testing.T) {
    ctx := context.Background()
    
    // 每个测试都能立即获得自己独立的数据库!
    testDB, testDBName, err := templateManager.CreateTestDatabase(ctx)
    if err != nil {
        t.Fatal(err)
    }
    defer testDB.Close()
    defer templateManager.DropTestDatabase(ctx, testDBName)
    
    // 在一个全新的数据库上运行你的测试逻辑
    repo := NewUserRepository(testDB)
    user, err := repo.CreateUser("test@example.com")
    // ... 你的断言
}

真实的性能提升:数据不会说谎

我们进行了广泛的基准测试,比较了传统的数据库创建方法与模板方法。结果不言而喻:

:rocket: 速度比较(数值越低越好)

模式复杂度 传统方法 模板方法 提升倍数
1 张表 28.9ms 28.2ms 1.03x
3 张表 39.5ms 27.6ms 1.43x
5 张表 43.1ms 28.8ms 1.50x

:chart_increasing: 扩展到数百个数据库

数据库数量 传统方法 模板方法 节省时间
20 个数据库 906.8ms 613.8ms 32%
50 个数据库 2.29s 1.53s 33%
200 个数据库 9.21s 5.84s 37%
500 个数据库 22.31s 14.82s 34%

无论模式复杂度如何,模板方法都能保持一致的性能,而传统方法则会随着你添加更多表、索引和约束而变得越来越慢。

主要特性

  • :high_voltage: 极速:在几毫秒内创建测试数据库,而不是几秒钟
  • :locked: 线程安全:非常适合并行测试执行(t.Parallel()
  • :counterclockwise_arrows_button: 双驱动支持:适用于 database/sql + pqpgx
  • :broom: 自动清理:全面清理测试数据库
  • :spouting_whale: 支持 Testcontainers:轻松与容器化的 PostgreSQL 集成
  • :bar_chart: 迁移灵活性:支持基于文件和自定义的迁移运行器

底层工作原理

  1. 初始化:创建一个模板数据库并一次性运行所有迁移
  2. 测试:为每个测试使用 CREATE DATABASE... TEMPLATE 创建一个新数据库
  3. 清理:删除测试数据库,最后清理模板

魔法发生在第 2 步 —— PostgreSQL 在文件系统级别处理模板数据库的复制,这比重复运行 SQL 迁移要快得多。

高级用法

与 testcontainers-go 一起使用

func setupTemplateManagerWithContainer(ctx context.Context) error {
    pgContainer, _ := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15"),
        postgres.WithDatabase("testdb"),
        // ... 其他配置
    )
    
    connStr, _ := pgContainer.ConnectionString(ctx, "sslmode=disable")
    
    provider := pgdbtemplate.NewStandardConnectionProvider(
        func(dbName string) string {
            return pgdbtemplate.ReplaceDatabaseInConnectionString(connStr, dbName)
        })
    
    // 像之前一样创建并使用模板管理器
    // ...
}

连接池选项

配置你的连接以获得最佳性能:

// 对于 database/sql + pq
provider := pgdbtemplate.NewStandardConnectionProvider(
    connStringFunc,
    pgdbtemplate.WithMaxOpenConns(25),
    pgdbtemplate.WithMaxIdleConns(10),
)

// 对于 pgx
provider := pgdbtemplate.NewPgxConnectionProvider(
    connStringFunc,
    pgdbtemplate.WithPgxMaxConns(10),
    pgdbtemplate.WithPgxMinConns(2),
)

何时应该使用 pgdbtemplate

  • 你的测试套件中有 >10 个数据库测试
  • 你的模式有 >3 张表,并且带有索引和关系
  • 你在开发过程中频繁运行测试
  • 你的 CI/CD 流水线包含数据库集成测试
  • 你重视快速反馈胜过咖啡休息时间

试试看!

准备好加速你的测试套件了吗?上手非常简单:

go get github.com/andrei-polukhin/pgdbtemplate

查看 GitHub 仓库 获取完整的文档、高级示例和贡献指南。

你在 Go 中使用 PostgreSQL 进行测试有什么经验? 你尝试过其他方法来加速你的测试套件吗?在下面的评论中分享你的想法!


更多关于Golang快速创建PostgreSQL测试数据库的工具pgdbtemplate的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang快速创建PostgreSQL测试数据库的工具pgdbtemplate的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


pgdbtemplate 确实是一个解决PostgreSQL测试数据库创建性能问题的优秀方案。它通过PostgreSQL的模板数据库特性,将数据库创建时间从线性增长变为近乎恒定,这对大型测试套件来说是个巨大的性能提升。

下面是一个更完整的集成示例,展示了如何在并行测试中使用它:

package user_test

import (
    "context"
    "database/sql"
    "fmt"
    "os"
    "testing"

    "github.com/andrei-polukhin/pgdbtemplate"
    _ "github.com/lib/pq"
)

var templateManager *pgdbtemplate.TemplateManager

func TestMain(m *testing.M) {
    ctx := context.Background()
    
    // 配置连接字符串生成函数
    connStringFunc := func(dbName string) string {
        return fmt.Sprintf(
            "postgres://%s:%s@%s/%s?sslmode=disable",
            os.Getenv("PG_USER"),
            os.Getenv("PG_PASSWORD"),
            os.Getenv("PG_HOST"),
            dbName,
        )
    }
    
    // 创建连接提供者(支持连接池配置)
    provider := pgdbtemplate.NewStandardConnectionProvider(
        connStringFunc,
        pgdbtemplate.WithMaxOpenConns(20),
        pgdbtemplate.WithMaxIdleConns(5),
    )
    
    // 配置迁移运行器
    migrationRunner := pgdbtemplate.NewFileMigrationRunner(
        []string{"./migrations"},
        pgdbtemplate.AlphabeticalMigrationFilesSorting,
    )
    
    // 创建模板管理器
    tm, err := pgdbtemplate.NewTemplateManager(pgdbtemplate.Config{
        ConnectionProvider: provider,
        MigrationRunner:    migrationRunner,
        TemplateDBName:     "test_template_db", // 自定义模板数据库名
    })
    if err != nil {
        panic(fmt.Sprintf("Failed to create template manager: %v", err))
    }
    
    // 初始化模板数据库(一次性操作)
    if err := tm.Initialize(ctx); err != nil {
        panic(fmt.Sprintf("Failed to initialize template: %v", err))
    }
    
    templateManager = tm
    exitCode := m.Run()
    
    // 清理模板数据库
    if err := tm.Cleanup(ctx); err != nil {
        fmt.Printf("Warning: cleanup failed: %v\n", err)
    }
    
    os.Exit(exitCode)
}

func TestUserRepository_CreateUser(t *testing.T) {
    t.Parallel() // 支持并行测试
    
    ctx := context.Background()
    
    // 为每个测试创建独立的数据库
    testDB, testDBName, err := templateManager.CreateTestDatabase(ctx)
    if err != nil {
        t.Fatalf("Failed to create test database: %v", err)
    }
    defer testDB.Close()
    defer templateManager.DropTestDatabase(ctx, testDBName)
    
    // 使用新数据库进行测试
    repo := NewUserRepository(testDB)
    
    // 测试逻辑
    user, err := repo.CreateUser(ctx, "test@example.com")
    if err != nil {
        t.Errorf("CreateUser failed: %v", err)
    }
    
    if user.Email != "test@example.com" {
        t.Errorf("Expected email test@example.com, got %s", user.Email)
    }
}

func TestUserRepository_GetUser(t *testing.T) {
    t.Parallel()
    
    ctx := context.Background()
    
    testDB, testDBName, err := templateManager.CreateTestDatabase(ctx)
    if err != nil {
        t.Fatalf("Failed to create test database: %v", err)
    }
    defer testDB.Close()
    defer templateManager.DropTestDatabase(ctx, testDBName)
    
    repo := NewUserRepository(testDB)
    
    // 先创建用户
    createdUser, err := repo.CreateUser(ctx, "get@example.com")
    if err != nil {
        t.Fatalf("Setup failed: %v", err)
    }
    
    // 测试查询
    fetchedUser, err := repo.GetUser(ctx, createdUser.ID)
    if err != nil {
        t.Errorf("GetUser failed: %v", err)
    }
    
    if fetchedUser.Email != "get@example.com" {
        t.Errorf("Expected email get@example.com, got %s", fetchedUser.Email)
    }
}

// 用户仓库示例
type UserRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) CreateUser(ctx context.Context, email string) (*User, error) {
    var id int64
    err := r.db.QueryRowContext(ctx,
        "INSERT INTO users (email) VALUES ($1) RETURNING id",
        email,
    ).Scan(&id)
    
    if err != nil {
        return nil, err
    }
    
    return &User{
        ID:    id,
        Email: email,
    }, nil
}

func (r *UserRepository) GetUser(ctx context.Context, id int64) (*User, error) {
    var user User
    err := r.db.QueryRowContext(ctx,
        "SELECT id, email FROM users WHERE id = $1",
        id,
    ).Scan(&user.ID, &user.Email)
    
    if err != nil {
        return nil, err
    }
    
    return &user, nil
}

type User struct {
    ID    int64
    Email string
}

对于需要事务隔离的测试,可以这样使用:

func TestUserRepository_WithTransaction(t *testing.T) {
    ctx := context.Background()
    
    testDB, testDBName, err := templateManager.CreateTestDatabase(ctx)
    if err != nil {
        t.Fatal(err)
    }
    defer testDB.Close()
    defer templateManager.DropTestDatabase(ctx, testDBName)
    
    // 在事务中运行测试
    tx, err := testDB.BeginTx(ctx, nil)
    if err != nil {
        t.Fatal(err)
    }
    defer tx.Rollback()
    
    repo := NewUserRepository(testDB)
    
    // 使用事务进行测试
    user, err := repo.CreateUser(ctx, "tx@example.com")
    if err != nil {
        t.Errorf("CreateUser in transaction failed: %v", err)
    }
    
    // 验证数据
    var count int
    err = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE email = $1", "tx@example.com").Scan(&count)
    if err != nil {
        t.Errorf("Query failed: %v", err)
    }
    
    if count != 1 {
        t.Errorf("Expected 1 user, got %d", count)
    }
}

这个库确实能显著提升测试性能,特别是在迁移复杂、测试数量多的情况下。通过模板数据库的方式,每个测试都能获得完全独立的数据库实例,同时避免了重复运行迁移的开销。

回到顶部