使用 PostgreSQL Docker 实例在 Go 中进行并行测试

在这篇文章中,我们介绍一下如何利用 Go 强大的 STLDockerSQL 事务PostgreSQL 存储库进行并行测试。

真实数据库与模拟测试的对比

拥有容器之前,我们很难做到使用真正的技术环境来代替模拟。对于数据库来说,通常每台机器上只有一个数据库服务器,每个应用程序单元上也只有一个数据库。

对于数据库交互的测试,模拟是目前最为普遍接受的一种解决方案。Mocks 可以帮助解决一些边缘情况,例如在执行过程中丢失连接等。无论是需要测试连接还是测试查询,来确保它们可以在真实的数据库中按照预期工作,我们都可以选择使用Docker,轻松的开始针对真实数据库的测试。

针对真实数据库进行测试的优缺点

优点:

  • 验证查询,可以确保在生产环境不出现意外情况
  • 识别可能出现的误差
  • 组合成复杂的场景,以确保我们的约束可以按照预期工作

缺点:

  • 管理数据库生命周期会产生一定的高成本

测试方法以及过程

测试思想

将每个包都视为一个代码单元,在项目中测试一个代码单元时,最终目的是确保它在生产中工作。

所需工具

  • Docker
  • go

在深入研究代码之前,我假设您已经熟悉 Go 及其强大的标准库。 这是一个示例存储库,您可以在其中找到并运行本文中介绍的测试。

基本设置

在运行包测试之前,必须实施对PostgreSQL 数据库的设置。

package psql_test

import (
    "fmt"
    "github.com/adrianbrad/psqldocker"
    "github.com/adrianbrad/psqltest"
    "log"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    // psql connection parameters.
    const (
        usr           = "usr"
        password      = "pass"
        dbName        = "tst"
        containerName = "psql_docker_tests"
    )

    // run a new psql docker container.
    c, err := psqldocker.NewContainer(
        usr,
        password,
        dbName,
        psqldocker.WithContainerName(containerName),
        psqldocker.WithSql(`
        CREATE TABLE users(
            user_id UUID PRIMARY KEY,
            email VARCHAR NOT NULL
        );`),
    )
    if err != nil {
        log.Fatalf("err while creating new psql container: %s", err)
    }

    // exit code
    var ret int

    defer func() {
        // close the psql container
        err = c.Close()
        if err != nil {
            log.Printf("err while tearing down db container: %s", err)
        }

        // exit with the code provided by executing m.Run().
        os.Exit(ret)
    }()

    // compose the psql dsn.
    dsn := fmt.Sprintf(
        "user=%s "+
            "password=%s "+
            "dbname=%s "+
            "host=localhost "+
            "port=%s "+
            "sslmode=disable",
        usr,
        password,
        dbName,
        c.Port(),
    )

    // register the psql container connection details
    // in order to be able to spawn new database connections
    // in an isolated transaction.
    psqltest.Register(dsn)

    // run the package tests.
    ret = m.Run()
}

这是在 TestMain 函数和以下外部包的帮助下实现的: psqldocker + psqltest

  • TestMain 允许开发人员在运行包测试之前或者之后,都可以运行任意代码。

  • psqldocker 以编程方式来管理 PostgreSQL Docker 容器的生命周期。 使用 psqldocker.NewContainer() 创建一个新的 PostgreSQL Docker 容器。在使用 m.Run()运行包测试后容器停止。

  • psqltest 是一组PostgreSQL测试实用程序,类似于 net/http/net/http/httptestpsqltest.Register() 函数是 DATA-DOG/go-txdb/db.go:Register()的包装器,它注册了一个新的SQL驱动程序,当使用它打开数据库连接时,将在隔离的SQL事务中启动该连接。

测试的实施

代码实现:

import (
    "testing"
    "github.com/adrianbrad/psqltest"
)

func TestUserRepository(t *testing.T) {
    t.Run("CreateUser", func(t *testing.T) {
        t.Parallel()

        t.Run("Success", func(t *testing.T) {
            t.Parallel()

            db := psqltest.NewTransactionTestingDB(t)
            ...
      })

      t.Run("InvalidID", func(t *testing.T) {
            t.Parallel()

            db := psqltest.NewTransactionTestingDB(t)
            ...
      })
   })

   t.Run("GetUser", func(t *testing.T) {
        t.Parallel()

          t.Run("Success", func(t *testing.T) {
             t.Parallel()

             db := psqltest.NewTransactionTestingDB(t)
             ...
      })

      t.Run("NotFound", func(t *testing.T) {
             t.Parallel()

             db := psqltest.NewTransactionTestingDB(t)
            ...
      })
   })
}

从代码中可以看出,每个测试都在单独的SQL事务中打开一个新的数据库连接,因此并行测试执行是安全的。这使得开发人员能够在多个测试中使用相同的数据库实体,例如我们可以在多个可以改变实体状态的测试中,使用相同的User实体。

在之前TestMain中,我们利用psqltest.Register()注册了驱动程序,现在我们通过psqltest.NewTransactionTestingDB()来创建一个新的数据库连接。

sql.Open()DSN(第二个输入参数)传入的是测试名称,它充当的是底层 SQL 事务的标识符。

使用同一个 DSN,开发人员可以打开多个数据库连接到同一个事务。例如,第一个测试中的标识符是TestUserRepository/CreateUser/Success

psqltest.NewTransactionTestingDB()实现代码

const driver = "pgsqltx"

// NewTransactionTestingDB returns a new transaction DB connection.
// It acts as a test helper and also takes care of closing the DB connection.
func NewTransactionTestingDB(t *testing.T) *sql.DB {
    // mark this function as a test helper
    t.Helper()

    // open a new database connection in a SQL transaction.
    db, err := sql.Open(driver, t.Name())
    if err != nil {
        t.Fatalf("open txdb conn: %s", err)
    }

    // ping the database to ensure connection validity.
    err = db.Ping()
    if err != nil {
        t.Fatalf("ping: %s", err)
    }

    // register a cleanup function that closes the database
    // connection thus reverting the transaction.
    t.Cleanup(func() {
        _ = db.Close()
    })

    return db
}

测试运行及结果

image.png 包中的测试./internal/psql都是针对真实的 PostgreSQL 数据库并行执行的。 这里的主要内容是执行时间,大约为4秒。执行时间包括使用 go 以编程方式启动和销毁一个新的 PostgreSQL 数据库。

总结

在本文中,主要向大家展示了如何使用 Docker 以编程方式启动拆卸 PostgreSQL 数据库。以及如何在单独的 SQL 事务中打开数据库连接,来实现对数据库实例执行的安全并行测试。

如果你想了解更多详细信息,可以移步:adrianbrad.medium.com/parallel-po…