之前 Hooks.Before/After 返回裸 error,调用方没法区分"钩子本身失败"和"SQL 执行失败"。改成返回 *HookError(包一层 exec.HookError),配合 errors.As 能精确识别。 同时给 Hooks 加一个 UseTx(dataset) (QueryFactory, error) 方法,在 Before 之前调用,允许钩子在写操作真正执行前替换掉默认连接(比如需要自动开事务包住 写操作和写后回调的场景),返回 nil, nil 表示沿用默认连接。目前只有 InsertDataset/UpdateDataset/DeleteDataset 会调用 UseTx,SelectDataset (只读)不需要。
150 lines
3.7 KiB
Go
150 lines
3.7 KiB
Go
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
|
||
}
|
||
|
||
// TestBeforeError:Before 失败时 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())
|
||
}
|
||
|
||
// TestAfterError:SQL 已经执行成功,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())
|
||
}
|
||
|
||
// TestUseTxSwapsConnection:UseTx 返回的 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())
|
||
}
|
||
|
||
// TestUseTxNil:UseTx 返回 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))
|
||
}
|