Skip to content

golang sqlmock extend

foolmacky edited this page Mar 24, 2023 · 2 revisions

■go-sqlmock proc outparam対応

何をしたいのか?

  • 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で入ってくる値

Match関数がやるべきこと

  • trueをリターンする
  • Execで与えられたparameterをResultOutで与えられた値で置換する

vはexecで与えられた値
matcherはResultOutで与えられているparamをArgument(interface)型に変換したもの
それによってMatch関数がcallされる
どの値に変換すべきかは、予めmatcherは知っていることができる
変化させる値は、exec時に与えられるSql.OutのDest: *pointer の実体側
sql.OutのDestはpointerで格納されている

MockOutの自動生成

		{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

Clone this wiki locally