英文:
Testing SQL table-valued parameters in sqlmock
问题
我有一个函数,旨在使用表值参数和存储过程将大量元素插入到 MSSQL 数据库中。
func (requester *Requester) doQuery(ctx context.Context, dtos interface{}) error {
conn, err := requester.conn.Conn(ctx)
if err != nil {
return err
}
defer func() {
if clErr := conn.Close(); clErr != nil {
err = clErr
}
}()
tx, err := conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: false})
if err != nil {
return err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
param := sql.Named("TVP", mssql.TVP{
TypeName: "MyTypeName",
Value: dtos,
})
return tx.ExecContext(ctx, "EXEC [dbo].[usp_InsertConsumption] @TVP", param)
}
我为这个函数编写的测试如下所示(请注意,它依赖于 ginkgo 和 gomega):
Describe("SQL Tests", func() {
It("AddConsumption - No failures - Added", func() {
db, mock, _ := sqlmock.New()
requester := Requester{conn: db}
defer db.Close()
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertConsumption] @TVP")).
WithArgs("").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertTags] @TVP")).
WithArgs("").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := requester.doQuery(context.TODO(), generateData())
Expect(err).ShouldNot(HaveOccurred())
Expect(mock.ExpectationsWereMet()).ShouldNot(HaveOccurred())
})
})
现在,这段代码是为 MySQL 上下文编写的,由于我将代码移植到 MSSQL,我一直遇到一个奇怪的错误:
sql: converting argument with name "TVP" type: unsupported type mssql.TVP, a struct
看起来 sqlmock 正在尝试对 TVP 对象调用 ConvertValue
,这是无效的。那么,我该如何使 sqlmock 正确处理这个值,以便我可以围绕查询进行单元测试?
英文:
I have a function that is designed to insert a large number of elements into an MSSQL database using a table-valued parameter and a procedure.
func (requester *Requester) doQuery(ctx context.Context, dtos interface{}) error {
conn, err := requester.conn.Conn(ctx)
if err != nil {
return err
}
defer func() {
if clErr := conn.Close(); clErr != nil {
err = clErr
}
}()
tx, err := conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: false})
if err != nil {
return err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
} else {
tx.Commit()
}()
param := sql.Named("TVP", mssql.TVP{
TypeName: "MyTypeName",
Value: dtos,
})
return tx.ExecContext(ctx, "EXEC [dbo].[usp_InsertConsumption] @TVP", param)
}
The test I wrote for this function is included below (note that it depends on ginkgo and gomega):
Describe("SQL Tests", func() {
It("AddConsumption - No failures - Added", func() {
db, mock, _ := sqlmock.New()
requester := Requester{conn: db}
defer db.Close()
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertConsumption] @TVP")).
WithArgs("").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertTags] @TVP")).
WithArgs("").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := requester.doQuery(context.TODO(), generateData())
Expect(err).ShouldNot(HaveOccurred())
Expect(mock.ExpectationsWereMet()).ShouldNot(HaveOccurred())
})
})
Now, this code was written for a MySQL context and since I've ported the code over to MSSQL, I've been getting a peculiar error:
sql: converting argument with name \"TVP\" type: unsupported type mssql.TVP, a struct
It appears that sqlmock is attempting to call ConvertValue
on the TVP object, which is invalid. So, how do I make sqlmock handle this value correctly so I can unit test around the query?
答案1
得分: 0
我在这里发现的是,sqlmock有一个名为ValueConverterOption
的函数,它接受driver.ValueConverter
接口的实现。这将在每次调用ConvertValue
时使用,代替标准函数。如果你想在ExecContext
函数接收到非标准参数时进行测试,比如在这种情况下使用TVP,那么你可以使用这个函数将自定义转换逻辑注入到sqlmock中。
type mockTvpConverter struct {}
func (converter *mockTvpConverter) ConvertValue(raw interface{}) (driver.Value, error) {
// 由于这个函数将替代每次调用ConvertValue的调用,我们将不可避免地从这个函数返回一个假字符串,所以我们需要检查我们是否收到了这个字符串或者一个TVP。可能需要更详细的逻辑
switch inner := raw.(type) {
case string:
return raw.(string), nil
case mssql.TVP:
// 首先,验证类型名称
Expect(inner.TypeName).Should(Equal("MyTypeName"))
// 在这里进行验证逻辑
// 最后,返回一个我们在验证参数时可以使用的假值
return "PASSED", nil
}
// 我们有一个无效的类型;返回一个错误
return nil, fmt.Errorf("Invalid type")
}
这意味着,测试代码变为:
Describe("SQL Tests", func() {
It("AddConsumption - No failures - Added", func() {
db, mock, _ := sqlmock.New(sqlmock.ValueConverterOption(&mockTvpConverter{}))
requester := Requester{conn: db}
defer db.Close()
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertConsumption] @TVP")).
WithArgs("PASSED").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertTags] @TVP")).
WithArgs("PASSED").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := requester.doQuery(context.TODO(), generateData())
Expect(err).ShouldNot(HaveOccurred())
Expect(mock.ExpectationsWereMet()).ShouldNot(HaveOccurred())
})
})
英文:
What I discovered here is that sqlmock has a function called ValueConverterOption
, accepts an implementation of the driver.ValueConverter
interface. This will be used in place of the standard function for every invocation of ConvertValue
. If you want to test around the ExecContext
function when it receives a non-standard argument, a TVP in this case, then you can use this function to inject custom conversion logic into sqlmock.
type mockTvpConverter struct {}
func (converter *mockTvpConverter) ConvertValue(raw interface{}) (driver.Value, error) {
// Since this function will take the place of every call of ConvertValue, we will inevitably
// the fake string we return from this function so we need to check whether we've recieved
// that or a TVP. More extensive logic may be required
switch inner := raw.(type) {
case string:
return raw.(string), nil
case mssql.TVP:
// First, verify the type name
Expect(inner.TypeName).Should(Equal("MyTypeName"))
// VERIFICATION LOGIC HERE
// Finally, return a fake value that we can use when verifying the arguments
return "PASSED", nil
}
// We had an invalid type; return an error
return nil, fmt.Errorf("Invalid type")
}
which means, the test then becomes:
Describe("SQL Tests", func() {
It("AddConsumption - No failures - Added", func() {
db, mock, _ := sqlmock.New(sqlmock.ValueConverterOption(&mockTvpConverter{}))
requester := Requester{conn: db}
defer db.Close()
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertConsumption] @TVP")).
WithArgs("PASSED").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(regexp.QuoteMeta("EXEC [dbo].[usp_InsertTags] @TVP")).
WithArgs("PASSED").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := requester.doQuery(context.TODO(), generateData())
Expect(err).ShouldNot(HaveOccurred())
Expect(mock.ExpectationsWereMet()).ShouldNot(HaveOccurred())
})
})
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论