golang PostgreSQL数据库单元测试模拟插件库pgxmock的使用
Golang PostgreSQL数据库单元测试模拟插件库pgxmock的使用
简介
pgxmock 是一个实现 pgx - PostgreSQL驱动和工具包 的模拟库。它基于著名的用于 sql/driver
的 sqlmock 库。
pgxmock 有且只有一个目的 - 在测试中模拟 pgx 的行为,而不需要实际的数据库连接。它有助于保持正确的 TDD 工作流程。
特点:
- 基于 go1.21 版本编写
- 不需要修改源代码
- 默认严格匹配期望顺序
- 除了 pgx 包外没有第三方依赖
安装
go get github.com/pashagolub/pgxmock/v4
使用示例
需要测试的代码
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
}
func main() {
// @NOTE: the real connection is not required for tests
db, err := pgx.Connect(context.Background(), "postgres://rolname@hostname/dbname")
if err != nil {
panic(err)
}
defer db.Close(context.Background())
if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
panic(err)
}
}
使用pgxmock进行测试
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查询匹配
// 使用自定义查询匹配器
mock, err := pgxmock.New(context.Background(), pgxmock.QueryMatcherOption(pgxmock.QueryMatcherEqual))
匹配时间类型参数
type AnyTime struct{}
// Match 实现sqlmock.Argument接口
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
许可证
三条款BSD许可证
更多关于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是一个用于pgx数据库驱动的模拟库,它允许你在不连接真实数据库的情况下进行单元测试。下面我将详细介绍如何使用pgxmock。
安装pgxmock
首先安装pgxmock库:
go get github.com/pashagolub/pgxmock
基本用法
1. 创建模拟连接
import (
"testing"
"github.com/pashagolub/pgxmock"
)
func TestSomething(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. 设置查询期望
// 设置期望查询并返回模拟行
mock.ExpectQuery("SELECT name FROM users WHERE id = \\$1").
WithArgs(1).
WillReturnRows(pgxmock.NewRows([]string{"name"}).AddRow("John"))
// 执行测试代码
var name string
err := mock.QueryRow(context.Background(), "SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
t.Errorf("error was not expected: %s", err)
}
if name != "John" {
t.Errorf("expected name to be 'John', got '%s'", name)
}
// 确保所有期望都满足
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
3. 设置执行期望
// 设置期望执行
mock.ExpectExec("UPDATE users SET name = \\$1 WHERE id = \\$2").
WithArgs("Jane", 1).
WillReturnResult(pgxmock.NewResult("UPDATE", 1))
// 执行测试代码
result, err := mock.Exec(context.Background(), "UPDATE users SET name = $1 WHERE id = $2", "Jane", 1)
if err != nil {
t.Errorf("error was not expected: %s", err)
}
affected := result.RowsAffected()
if affected != 1 {
t.Errorf("expected 1 affected row, got %d", affected)
}
完整示例
下面是一个完整的存储库层单元测试示例:
package repository
import (
"context"
"testing"
"github.com/jackc/pgx/v4"
"github.com/pashagolub/pgxmock"
"github.com/stretchr/testify/assert"
)
type UserRepository struct {
conn *pgx.Conn
}
func NewUserRepository(conn *pgx.Conn) *UserRepository {
return &UserRepository{conn: conn}
}
func (r *UserRepository) GetUserName(id int) (string, error) {
var name string
err := r.conn.QueryRow(context.Background(), "SELECT name FROM users WHERE id = $1", id).Scan(&name)
return name, err
}
func TestUserRepository_GetUserName(t *testing.T) {
mock, err := pgxmock.NewConn()
if err != nil {
t.Fatalf("failed to create mock: %v", err)
}
defer mock.Close(context.Background())
// 设置期望
mock.ExpectQuery("SELECT name FROM users WHERE id = \\$1").
WithArgs(1).
WillReturnRows(pgxmock.NewRows([]string{"name"}).AddRow("John Doe"))
repo := NewUserRepository(mock)
name, err := repo.GetUserName(1)
assert.NoError(t, err)
assert.Equal(t, "John Doe", name)
// 确保所有期望都满足
assert.NoError(t, mock.ExpectationsWereMet())
}
高级功能
1. 事务测试
func TestTransaction(t *testing.T) {
mock, err := pgxmock.NewConn()
if err != nil {
t.Fatalf("failed to create mock: %v", err)
}
defer mock.Close(context.Background())
// 期望开始事务
mock.ExpectBegin()
// 期望在事务中的查询
mock.ExpectQuery("SELECT name FROM users WHERE id = \\$1").
WithArgs(1).
WillReturnRows(pgxmock.NewRows([]string{"name"}).AddRow("John"))
// 期望提交事务
mock.ExpectCommit()
// 执行测试代码
tx, err := mock.Begin(context.Background())
if err != nil {
t.Fatalf("failed to begin transaction: %v", err)
}
var name string
err = tx.QueryRow(context.Background(), "SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
tx.Rollback(context.Background())
t.Fatalf("failed to query: %v", err)
}
err = tx.Commit(context.Background())
if err != nil {
t.Fatalf("failed to commit: %v", err)
}
assert.NoError(t, mock.ExpectationsWereMet())
}
2. 错误模拟
func TestErrorCase(t *testing.T) {
mock, err := pgxmock.NewConn()
if err != nil {
t.Fatalf("failed to create mock: %v", err)
}
defer mock.Close(context.Background())
// 模拟查询错误
mock.ExpectQuery("SELECT name FROM users WHERE id = \\$1").
WithArgs(1).
WillReturnError(fmt.Errorf("some error"))
// 执行测试代码
var name string
err = mock.QueryRow(context.Background(), "SELECT name FROM users WHERE id = $1", 1).Scan(&name)
assert.Error(t, err)
assert.Equal(t, "some error", err.Error())
assert.NoError(t, mock.ExpectationsWereMet())
}
最佳实践
- 每个测试用例创建新的mock:避免测试间的相互影响
- 总是检查ExpectationsWereMet:确保所有预期的查询/执行都被调用
- 使用正则表达式匹配查询:使用
\\$
而不是$
来匹配参数占位符 - 清理资源:记得调用
Close()
或使用defer - 结合测试框架:如testify/assert可以简化断言代码
pgxmock是测试pgx数据库交互的强大工具,它可以帮助你编写快速、隔离的单元测试,而无需依赖真实的数据库连接。