golang PostgreSQL数据库单元测试模拟插件库pgxmock的使用

Golang PostgreSQL数据库单元测试模拟插件库pgxmock的使用

简介

pgxmock是一个实现pgx(PostgreSQL驱动和工具包)的模拟库,基于著名的sqlmock库(sql/driver)。它的唯一目的是在测试中模拟pgx行为,而不需要真实的数据库连接,有助于保持正确的TDD工作流程。

特点:

  • 基于go1.21版本编写
  • 不需要修改源代码
  • 默认严格匹配期望顺序
  • 除了pgx包外没有第三方依赖

安装

go get github.com/pashagolub/pgxmock/v4

使用示例

基础示例

首先我们有一个需要测试的函数recordStats,它会更新产品视图并插入产品查看者记录:

package main

import (
	"context"

	pgx "github.com/jackc/pgx/v5"
)

type PgxIface interface {
	Begin(context.Context) (pgx.Tx, error)
	Close(context.Context) error
}

func recordStats(db PgxIface, userID, productID int) (err error) {
	if tx, err := db.Begin(context.Background()); err != nil {
		return
	}
	defer func() {
		switch err {
		case nil:
			err = tx.Commit(context.Background())
		default:
			_ = tx.Rollback(context.Background())
		}
	}()
	sql := "UPDATE products SET views = views + 1"
	if _, err = tx.Exec(context.Background(), sql); err != nil {
		return
	}
	sql = "INSERT INTO product_viewers (user_id, product_id) VALUES ($1, $2)"
	if _, err = tx.Exec(context.Background(), sql, userID, productID); err != nil {
		return
	}
	return
}

测试代码

package main

import (
	"context"
	"fmt"
	"testing"

	"github.com/pashagolub/pgxmock/v4"
)

// 成功测试用例
func TestShouldUpdateStats(t *testing.T) {
	mock, err := pgxmock.NewPool()
	if err != nil {
		t.Fatal(err)
	}
	defer mock.Close()

	// 设置期望
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE products").
		WillReturnResult(pgxmock.NewResult("UPDATE", 1))
	mock.ExpectExec("INSERT INTO product_viewers").
		WithArgs(2, 3).
		WillReturnResult(pgxmock.NewResult("INSERT", 1))
	mock.ExpectCommit()

	// 执行测试方法
	if err = recordStats(mock, 2, 3); err != nil {
		t.Errorf("error was not expected while updating: %s", err)
	}

	// 确保所有期望都满足
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

// 失败测试用例
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
	mock, err := pgxmock.NewPool()
	if err != nil {
		t.Fatal(err)
	}
	defer mock.Close()

	// 设置期望
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE products").
		WillReturnResult(pgxmock.NewResult("UPDATE", 1))
	mock.ExpectExec("INSERT INTO product_viewers").
		WithArgs(2, 3).
		WillReturnError(fmt.Errorf("some error"))
	mock.ExpectRollback()

	// 执行测试方法
	if err = recordStats(mock, 2, 3); err == nil {
		t.Errorf("was expecting an error, but there was none")
	}

	// 确保所有期望都满足
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

自定义SQL查询匹配

pgxmock允许自定义SQL查询匹配器:

mock, err := pgxmock.New(context.Background(), pgxmock.QueryMatcherOption(pgxmock.QueryMatcherEqual))

默认使用正则表达式匹配(pgxmock.QueryMatcherRegexp),也可以使用完全匹配(pgxmock.QueryMatcherEqual)。

匹配特殊参数类型

对于像time.Time这样的特殊类型参数,可以实现Argument接口:

type AnyTime struct{}

// Match satisfies sqlmock.Argument interface
func (a AnyTime) Match(v interface{}) bool {
	_, ok := v.(time.Time)
	return ok
}

func TestAnyTimeArgument(t *testing.T) {
	t.Parallel()
	db, mock, err := New()
	if err != nil {
		t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
	}
	defer db.Close()

	mock.ExpectExec("INSERT INTO users").
		WithArgs("john", AnyTime{}).
		WillReturnResult(NewResult(1, 1))

	_, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now())
	if err != nil {
		t.Errorf("error '%s' was not expected, while inserting a row", err)
	}

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

运行测试

go test -race

pgxmock是一个强大的PostgreSQL数据库模拟库,可以帮助开发者在不连接真实数据库的情况下进行单元测试,提高测试效率和可靠性。


更多关于golang PostgreSQL数据库单元测试模拟插件库pgxmock的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang PostgreSQL数据库单元测试模拟插件库pgxmock的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


使用pgxmock进行Golang PostgreSQL单元测试

pgxmock是一个用于测试PostgreSQL数据库交互的模拟库,特别适合与pgx库一起使用。它允许你在不连接真实数据库的情况下测试数据库相关代码。

安装pgxmock

首先安装pgxmock库:

go get github.com/pashagolub/pgxmock

基本用法

1. 创建模拟连接

import (
	"testing"
	"github.com/pashagolub/pgxmock"
)

func TestDatabaseOperation(t *testing.T) {
	mock, err := pgxmock.NewConn()
	if err != nil {
		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
	}
	defer mock.Close(context.Background())
	
	// 在这里设置期望和测试
}

2. 设置查询期望

func TestGetUser(t *testing.T) {
	mock, err := pgxmock.NewConn()
	if err != nil {
		t.Fatal(err)
	}
	defer mock.Close(context.Background())

	// 设置期望查询和返回的行
	rows := mock.NewRows([]string{"id", "name", "email"}).
		AddRow(1, "John Doe", "john@example.com").
		AddRow(2, "Jane Doe", "jane@example.com")

	mock.ExpectQuery("SELECT id, name, email FROM users WHERE id = \\$1").
		WithArgs(1).
		WillReturnRows(rows)

	// 调用实际函数
	user, err := GetUser(mock, 1)
	if err != nil {
		t.Errorf("error was not expected while getting user: %s", err)
	}

	// 验证期望
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
	
	// 验证返回的数据
	if user.Name != "John Doe" {
		t.Errorf("expected name to be John Doe, got %s", user.Name)
	}
}

3. 测试事务

func TestTransaction(t *testing.T) {
	mock, err := pgxmock.NewConn()
	if err != nil {
		t.Fatal(err)
	}
	defer mock.Close(context.Background())

	// 期望事务开始
	mock.ExpectBegin()
	
	// 期望查询
	mock.ExpectQuery("SELECT balance FROM accounts WHERE id = \\$1").
		WithArgs(1).
		WillReturnRows(mock.NewRows([]string{"balance"}).AddRow(100))
	
	// 期望更新
	mock.ExpectExec("UPDATE accounts SET balance = balance - \\$1 WHERE id = \\$2").
		WithArgs(50, 1).
		WillReturnResult(pgxmock.NewResult("UPDATE", 1))
	
	// 期望事务提交
	mock.ExpectCommit()

	// 调用实际函数
	err = TransferMoney(mock, 1, 2, 50)
	if err != nil {
		t.Errorf("error was not expected while transferring money: %s", err)
	}

	// 验证所有期望
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

4. 测试错误场景

func TestQueryError(t *testing.T) {
	mock, err := pgxmock.NewConn()
	if err != nil {
		t.Fatal(err)
	}
	defer mock.Close(context.Background())

	// 设置期望查询返回错误
	expectedErr := errors.New("connection failed")
	mock.ExpectQuery("SELECT \\* FROM users").
		WillReturnError(expectedErr)

	// 调用实际函数
	_, err = GetAllUsers(mock)
	if err == nil {
		t.Error("expected error but got none")
	}
	
	if err != expectedErr {
		t.Errorf("expected error '%v' but got '%v'", expectedErr, err)
	}

	// 验证期望
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

高级用法

1. 使用匹配器

func TestWithMatcher(t *testing.T) {
	mock, err := pgxmock.NewConn()
	if err != nil {
		t.Fatal(err)
	}
	defer mock.Close(context.Background())

	// 使用AnyArg匹配任意参数
	mock.ExpectExec("UPDATE users SET name = \\$1 WHERE id = \\$2").
		WithArgs(pgxmock.AnyArg(), 1).
		WillReturnResult(pgxmock.NewResult("UPDATE", 1))

	// 调用实际函数
	err = UpdateUserName(mock, 1, "New Name")
	if err != nil {
		t.Errorf("error was not expected while updating user: %s", err)
	}
}

2. 测试预处理语句

func TestPreparedStatement(t *testing.T) {
	mock, err := pgxmock.NewConn()
	if err != nil {
		t.Fatal(err)
	}
	defer mock.Close(context.Background())

	// 期望预处理语句
	mock.ExpectPrepare("get_user", "SELECT name FROM users WHERE id = \\$1")
	
	// 期望执行预处理语句
	rows := mock.NewRows([]string{"name"}).AddRow("John Doe")
	mock.ExpectQuery("get_user").
		WithArgs(1).
		WillReturnRows(rows)

	// 调用实际函数
	name, err := GetUserName(mock, 1)
	if err != nil {
		t.Errorf("error was not expected while getting user name: %s", err)
	}
	
	if name != "John Doe" {
		t.Errorf("expected name to be John Doe, got %s", name)
	}
}

最佳实践

  1. 每个测试用例创建新的模拟连接:避免测试之间的状态污染
  2. 总是检查ExpectationsWereMet:确保所有期望的查询都被执行
  3. 使用defer关闭连接:防止资源泄漏
  4. 测试错误场景:不仅要测试成功路径,还要测试错误处理
  5. 保持测试独立:每个测试应该只关注一个功能点

pgxmock是一个强大的工具,可以让你在不依赖真实数据库的情况下彻底测试数据库交互代码。通过合理设置期望和验证结果,你可以确保你的数据库层代码在各种情况下都能正确工作。

回到顶部