Files
db/exec/query_hooks_test.go
what 6ed2803158 feat: Hooks 新增 UseTx,Before/After 改用类型化的 HookError
之前 Hooks.Before/After 返回裸 error,调用方没法区分"钩子本身失败"和"SQL
执行失败"。改成返回 *HookError(包一层 exec.HookError),配合 errors.As
能精确识别。

同时给 Hooks 加一个 UseTx(dataset) (QueryFactory, error) 方法,在 Before
之前调用,允许钩子在写操作真正执行前替换掉默认连接(比如需要自动开事务包住
写操作和写后回调的场景),返回 nil, nil 表示沿用默认连接。目前只有
InsertDataset/UpdateDataset/DeleteDataset 会调用 UseTx,SelectDataset
(只读)不需要。
2026-08-20 17:08:29 +08:00

150 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package exec_test
import (
"errors"
"testing"
dbv2 "git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exec"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/suite"
)
// mockHooks 是 exec.Hooks 的最小实现,用来验证 UseTx/Before/After 与 db.HookError 的接线是否正确
type mockHooks struct {
useTxFactory exec.QueryFactory
useTxErr error
beforeErr error
afterErr error
useTxCalls int
beforeCalls int
afterCalls int
}
func (h *mockHooks) UseTx(dataset interface{}) (exec.QueryFactory, error) {
h.useTxCalls++
return h.useTxFactory, h.useTxErr
}
func (h *mockHooks) Before(dataset interface{}) *exec.HookError {
h.beforeCalls++
if h.beforeErr != nil {
return &exec.HookError{Err: h.beforeErr}
}
return nil
}
func (h *mockHooks) After(dataset interface{}, result interface{}) *exec.HookError {
h.afterCalls++
if h.afterErr != nil {
return &exec.HookError{Err: h.afterErr}
}
return nil
}
type hooksSuite struct {
suite.Suite
}
// TestBeforeErrorBefore 失败时 SQL 不会被执行,error 能用 errors.As 解出 db.HookError
func (hs *hooksSuite) TestBeforeError() {
mDB, mock, err := sqlmock.New()
hs.NoError(err)
hooks := &mockHooks{beforeErr: errors.New("before failed")}
ds := dbv2.New("mock", mDB).Insert("items").
Rows(dbv2.Record{"name": "Test1"}).
WithHook(hooks)
_, err = ds.Executor().Exec()
hs.Error(err)
var hookErr *dbv2.HookError
hs.True(errors.As(err, &hookErr))
hs.Equal("before failed", hookErr.Unwrap().Error())
hs.Equal(1, hooks.beforeCalls)
hs.Equal(0, hooks.afterCalls)
hs.NoError(mock.ExpectationsWereMet())
}
// TestAfterErrorSQL 已经执行成功,After 失败时 error 依然能用 errors.As 解出 db.HookError
// 且 result 仍然可用(区别于 SQL 本身执行失败)
func (hs *hooksSuite) TestAfterError() {
mDB, mock, err := sqlmock.New()
hs.NoError(err)
mock.ExpectExec(`INSERT INTO "items"`).WillReturnResult(sqlmock.NewResult(1, 1))
hooks := &mockHooks{afterErr: errors.New("after failed")}
ds := dbv2.New("mock", mDB).Insert("items").
Rows(dbv2.Record{"name": "Test1"}).
WithHook(hooks)
result, err := ds.Executor().Exec()
hs.Error(err)
hs.NotNil(result)
rows, rowsErr := result.RowsAffected()
hs.NoError(rowsErr)
hs.EqualValues(1, rows)
var hookErr *dbv2.HookError
hs.True(errors.As(err, &hookErr))
hs.Equal("after failed", hookErr.Unwrap().Error())
hs.NoError(mock.ExpectationsWereMet())
}
// TestUseTxSwapsConnectionUseTx 返回的 QueryFactory 会替换默认连接,SQL 应该在返回的
// factory 上执行,原始连接完全不会被用到
func (hs *hooksSuite) TestUseTxSwapsConnection() {
origDB, origMock, err := sqlmock.New()
hs.NoError(err)
txDB, txMock, err := sqlmock.New()
hs.NoError(err)
txMock.ExpectExec(`INSERT INTO "items"`).WillReturnResult(sqlmock.NewResult(1, 1))
hooks := &mockHooks{useTxFactory: exec.NewQueryFactory(txDB)}
ds := dbv2.New("mock", origDB).Insert("items").
Rows(dbv2.Record{"name": "Test1"}).
WithHook(hooks)
result, err := ds.Executor().Exec()
hs.NoError(err)
hs.NotNil(result)
hs.Equal(1, hooks.useTxCalls)
hs.NoError(txMock.ExpectationsWereMet())
hs.NoError(origMock.ExpectationsWereMet())
}
// TestUseTxNilUseTx 返回 nil factory 时沿用默认连接
func (hs *hooksSuite) TestUseTxNil() {
mDB, mock, err := sqlmock.New()
hs.NoError(err)
mock.ExpectExec(`INSERT INTO "items"`).WillReturnResult(sqlmock.NewResult(1, 1))
hooks := &mockHooks{}
ds := dbv2.New("mock", mDB).Insert("items").
Rows(dbv2.Record{"name": "Test1"}).
WithHook(hooks)
_, err = ds.Executor().Exec()
hs.NoError(err)
hs.Equal(1, hooks.useTxCalls)
hs.NoError(mock.ExpectationsWereMet())
}
func TestHooks(t *testing.T) {
suite.Run(t, new(hooksSuite))
}