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
更多关于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)
}
}
最佳实践
- 每个测试用例创建新的模拟连接:避免测试之间的状态污染
- 总是检查ExpectationsWereMet:确保所有期望的查询都被执行
- 使用defer关闭连接:防止资源泄漏
- 测试错误场景:不仅要测试成功路径,还要测试错误处理
- 保持测试独立:每个测试应该只关注一个功能点
pgxmock是一个强大的工具,可以让你在不依赖真实数据库的情况下彻底测试数据库交互代码。通过合理设置期望和验证结果,你可以确保你的数据库层代码在各种情况下都能正确工作。