-
Notifications
You must be signed in to change notification settings - Fork 0
golang sqlmock extend
foolmacky edited this page Mar 24, 2023
·
2 revisions
何をしたいのか?
- mockに対して、期待するAction, 期待されるParam0がセットされる
- db.Execが実行され、Param1が与えられる
- 与えられたパラメータがチェックされるが無視される
- param1の内容がparam0に置き換えられる
ExpectExec{
queryBasedExpectation{
commonExpectation{
sync.Mutex
triggered bool
err error
}
}
expectSQL string
converter driver.ValueConverter
args []driver.Value
}
result driver.Result
delay time.Duration
}
db, mock, err := New()
mock.ExceptExec("").WithArgs(param0).WithOutArgs(param1)
db.ExecOut("", param0)
--param0の値がparam1に置換される
db.Exec("", param0)
param0 = param1
独自タイプの場合、driverにわたす前にエラーになってしまう
2020/07/25 01:24:51 error : sql: converting argument with name "out" type: unsupported type sqlmock.anyArgument, a struct
2020/07/25 01:11:43 error : sql: converting argument with name "out" type: unsupported type main.MockOut, a struct
expectは nil, execはsql.Outの場合
2020/07/25 02:01:52 error : ExecQuery 'insert into hoge', arguments do not match: argument 2 expected [<nil> - <nil>] does not match actual [sql.Out - {_Named_Fields_Required:{} Dest:<nil> In:false}]
expect , exec ともパラメータがsql.Outの場合
driverにはそのまま渡されるが、Match不能でエラーになる
2020/07/25 02:05:50 error : ExecQuery 'insert into hoge', arguments do not match: could not convert 2 argument sql.Out - {_Named_Fields_Required:{} Dest:10 In:false} to driver value: unsupported type sql.Out, a struct
expectをAnyArgにして、実際のexecをsql.Outで与えれば、マッチングはOKになる。
あとは内部でparamを強制的に書き換える
args := []sql.NamedArg{
{Name: "name", Value: "foolmacky"},
{Name: "age", Value: 50},
{Name: "out", Value: sqlmock.AnyArg()},
}
args0 := []sql.NamedArg{
{Name: "name", Value: "foolmacky"},
{Name: "age", Value: 50},
{Name: "out", Value: sql.Out{Dest: 10}},
}
このパターンはだめ
args := []sql.NamedArg{
{Name: "name", Value: "foolmacky"},
{Name: "age", Value: 50},
{Name: "out", Value: sql.Out{Dest: sqlmock.AnyArg()}},
}
args0 := []sql.NamedArg{
{Name: "name", Value: "foolmacky"},
{Name: "age", Value: 50},
{Name: "out", Value: sql.Out{Dest: 10}},
}
- expectのパラメータはsql.OutをAnyArgに置換
- argを複製してdriver
2020/07/25 02:49:39 error : ExecQuery 'insert into hoge', arguments do not match: matcher main.MockOut could not match 2 argument driver.NamedValue - {Name:out Ordinal:3 Value:{_Named_Fields_Required:{} Dest:10 In:false}}
args := []sql.NamedArg{
{Name: "name", Value: "foolmacky"},
{Name: "age", Value: 50},
{Name: "out", Value: MockOut{Out: sql.Out{Dest: 20}}},
// {Name: "out", Value: sql.Out{Dest: sqlmock.AnyArg()}},
}
args0 := []sql.NamedArg{
{Name: "name", Value: "foolmacky"},
{Name: "age", Value: 50},
{Name: "out", Value: sql.Out{Dest: 10}},
}
// @TODO should we assert either all args are named or ordinal?
for k, v := range args {
// custom argument matcher
matcher, ok := e.args[k].(Argument)
if ok {
if !matcher.Match(v.Value) {
return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k])
}
continue
}
//macher : expectで入れた値
//v : 実際のexecで入ってくる値
- trueをリターンする
- Execで与えられたparameterをResultOutで与えられた値で置換する
vはexecで与えられた値
matcherはResultOutで与えられているparamをArgument(interface)型に変換したもの
それによってMatch関数がcallされる
どの値に変換すべきかは、予めmatcherは知っていることができる
変化させる値は、exec時に与えられるSql.OutのDest: *pointer の実体側
sql.OutのDestはpointerで格納されている
{Name: "out", Value: MockOut{Dest: 20, In: true, Pmatch: true}},
import (
"database/sql"
"database/sql/driver"
"fmt"
"log"
"github.com/DATA-DOG/go-sqlmock"
)
type ProcMock struct {
sqlmock.Sqlmock
}
type ExpectedProcExec struct {
*sqlmock.ExpectedExec
namedIn []sql.NamedArg
}
func (p *ProcMock) ExpectProcExec(expectedSQL string) *ExpectedProcExec {
return &ExpectedProcExec{p.Sqlmock.ExpectExec(expectedSQL), []sql.NamedArg{}}
}
func CopySqlParams(params []sql.NamedArg) []sql.NamedArg {
cparams := make([]sql.NamedArg, len(params))
for i, param := range params {
switch param.Value.(type) {
case sql.Out:
nIn := param.Value.(sql.Out).In
var nDest interface{}
switch param.Value.(sql.Out).Dest.(type) {
case *int:
dest := *(param.Value.(sql.Out).Dest.(*int))
nDest = &dest
case *float64:
dest := *(param.Value.(sql.Out).Dest.(*float64))
nDest = &dest
case *string:
dest := *(param.Value.(sql.Out).Dest.(*string))
nDest = &dest
case *bool:
dest := *(param.Value.(sql.Out).Dest.(*bool))
nDest = &dest
default:
nDest = nil
}
nOut := sql.Out{Dest: nDest, In: nIn}
cparams[i] = sql.NamedArg{Name: param.Name, Value: nOut}
default:
cparams[i] = sql.NamedArg{Name: param.Name, Value: param.Value}
}
}
return cparams
}
func (e *ExpectedProcExec) WithValuesOut(args []sql.NamedArg, match bool) *ExpectedProcExec {
for _, arg := range args {
for i, named := range e.namedIn {
if named.Name == arg.Name {
_, ok := named.Value.(sql.Out)
if !ok {
continue
}
_, ok = arg.Value.(sql.Out)
if !ok {
continue
}
in := arg.Value.(sql.Out).In
switch dest := arg.Value.(sql.Out).Dest.(type) {
case *int:
e.namedIn[i].Value = MockOut{Dest: *dest, In: in, Pmatch: match}
case *float64:
e.namedIn[i].Value = MockOut{Dest: *dest, In: in, Pmatch: match}
case *string:
e.namedIn[i].Value = MockOut{Dest: *dest, In: in, Pmatch: match}
default:
e.namedIn[i].Value = MockOut{Dest: nil, In: in, Pmatch: match}
}
}
}
}
params := driverValues(e.namedIn)
e.ExpectedExec.WithArgs(params...)
return e
}
func transInterface(params []sql.NamedArg) []interface{} {
binds := make([]interface{}, len(params))
for i, bind := range params {
binds[i] = bind
}
return binds
}
func driverValues(params []sql.NamedArg) []driver.Value {
binds := make([]driver.Value, len(params))
for i, bind := range params {
binds[i] = bind.Value
}
return binds
}
func (e *ExpectedProcExec) WithNamedArgs(args []sql.NamedArg) *ExpectedProcExec {
params := driverValues(args)
e.ExpectedExec.WithArgs(params...)
e.namedIn = CopySqlParams(args)
return e
}
func (e *ExpectedProcExec) WillReturnResult(result driver.Result) *ExpectedProcExec {
e.ExpectedExec.WillReturnResult(result)
return e
}
func NewProcMock() (*sql.DB, ProcMock, error) {
db, mock, _ := sqlmock.New()
return db, ProcMock{mock}, nil
}
//必ずaddressを渡す
func OutParam(out interface{}, in bool) sql.Out {
switch out.(type) {
case *int, *float64, *string, *bool:
return sql.Out{Dest: out, In: in}
default:
return sql.Out{Dest: nil, In: in}
}
}
type MockOut struct {
Dest interface{}
In bool
Pmatch bool
}
func (mo MockOut) Match(v driver.Value) bool {
log.Println("matching...")
fmt.Printf("%v\n", v)
fmt.Printf("%v\n", mo.Dest)
out, ok := v.(sql.Out)
if !ok {
return false
}
if out.In != mo.In {
return false
}
switch dest := out.Dest.(type) {
case *int:
pi, ok := mo.Dest.(int)
if !ok {
return false
}
*dest = pi
case *float64:
pf, ok := mo.Dest.(float64)
if !ok {
return false
}
*dest = pf
case *string:
ps, ok := mo.Dest.(string)
if !ok {
return false
}
*dest = ps
case *bool:
pb, ok := mo.Dest.(bool)
if !ok {
return false
}
*dest = pb
default:
return false
}
return mo.Pmatch
}
func main() {
//driver.Valueで値を処理する都合上、named paramと言いつつ、expectとexecに渡すparamは順番が揃っていないといけない
var outVal int = 10
var myName string = "foolmacky"
args0 := []sql.NamedArg{
{Name: "name", Value: OutParam(&myName, true)},
{Name: "age", Value: 50},
{Name: "out", Value: OutParam(&outVal, true)},
}
fmt.Printf("Param name : %s\n", myName)
fmt.Printf("Param out : %d\n", outVal)
var retVal int = 20
var trueName string = "fakemacky"
out := []sql.NamedArg{
{Name: "name", Value: OutParam(&trueName, true)},
{Name: "out", Value: OutParam(&retVal, true)},
}
db, mock, _ := NewProcMock()
mock.ExpectBegin()
mock.ExpectProcExec("insert into hoge").WithNamedArgs(args0).WithValuesOut(out, true).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
tx, err := db.Begin()
_, err = tx.Exec("insert into hoge", transInterface(args0)...)
if err != nil {
log.Println(fmt.Sprintf("error : %s", err))
}
tx.Commit()
fmt.Printf("Param name : %s\n", myName)
fmt.Printf("Param out : %d\n", outVal)
}
実行結果
Param name : foolmacky
Param out : 10
2023/03/24 19:08:50 matching...
{{} 0xc000010450 true}
fakemacky
2023/03/24 19:08:50 matching...
{{} 0xc000016120 true}
20
Param name : fakemacky
Param out : 20