测试 sqlmock 中的 SQL 表值参数

huangapple go评论95阅读模式
英文:

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())
	})
})

huangapple
  • 本文由 发表于 2020年11月25日 18:15:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/65002589.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定