go mock mysql_go sqlmocks的使用

本文介绍了如何使用go-sqlmock库来模拟MySQL数据库交互,以避免在测试中实际操作数据库。通过示例展示了如何设置预期查询和返回结果,以及解决beego ORM在插入或更新时忽略主键的问题。通过调试源码,发现beego ORM在遇到字段tag包含`auto`时会过滤主键,建议将`auto`改为`pk`来标识主键。
摘要由CSDN通过智能技术生成

本文首发于:科学世纪的炼金工坊​yuchanns.org

今天q群一哥们儿说,他使用beego orm的InserOrUpdate的时候出现了相同主键还是会执行新增插入的bug,找我帮忙看看什么情况。

当时我的第一反应是让他先在debug模式下打印sql语句看看有没有什么问题,但小伙子可能是比较紧张一直打印不出来。

由于我当时不在生产电脑前,对beego也不是很熟悉,只能临时用普通电脑装一个go,设置一下环境拉一下代码写一个测试用例。因为安装mysql太麻烦了,所以我打算简单的用DATA-DOG/go-sqlmock来mock数据库返回。

于是就顺手写一下使用记录,算是给那位大兄弟的一个教程科普吧。

情景简述

案例情景介绍如下:有一个TExchangeInfo结构体,实例化后填充数据,然后执行InsertOrUpdate,当数据存在时,使用更新,当数据不存在时才插入:

type TExchangeInfo struct {

ID int64 `orm:"column(id);auto"`

DeparmentID int64 `orm:"column(deparment_id)"`

Times uint `orm:"column(times)"`

Number uint `orm:"column(number)"`

Lastmodified time.Time `orm:"column(lastmodified);type(datetime);auto_now"`

}

sqlmock使用

sqlmock的使用其实很简单,参照文档就可以。我这里简单说明一下。

首先大家都知道,go标准库有一个datebase/sql/driver包,内部定义了数据库驱动标准接口,不管什么方言的数据库,只要实现了这些接口,就可以统一调用接口定制的方法来进行数据库交互。

而sqlmock也是通过sqlmock.New()这个方法返回一个标准的sql.DB结构体实例指针,这是一个数据库连接句柄。当然除此之外还返回了一个sqlmock.Sqlmock结构体实例。

而我们拿到*sql.DB之后,就可以递交给orm来使用了。

以beego orm为例,它有一个orm.NewOrmWithDB方法,用来实例化并指定连接句柄。

func InsertOrUpdatePrintSql() error {

db, mock, err := sqlmock.New()

if err != nil {

return err

}

defer db.Close()

orm.Debug = true // 开启debug模式才能打印出拼装的sql语句 o, err := orm.NewOrmWithDB("mysql", "default", db)

if err != nil {

return err

}

}

写到这里,似乎我们已经能够和往常一样使用orm了。试着写一个测试用例运行这个函数,结果会发现报错了,一个panic:

panic: all expectations were already fulfilled, call to Prepare 'SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP)' query was not expected [recovered]

一时之间令人摸不着头脑?这和接下来我们要讲的sqlmock.Sqlmock有关。

mock数据

mock的核心就在于mock这个词,也就是说,屏蔽上游细节,使用一些实现设定好的数据来模拟上游返回的数据。

sqlmock也同样如此,你需要在mock测试过程中,指定你期望(Expectations)执行的查询语句,以及假定的返回结果(WillReturnResult)。注:beego orm在启动时候,会先执行SELECT TIMEDIFF...和SELECT ENGINE...两个语句,所以我们也需要把它添加到我们的期望中。

func InsertOrUpdatePrintSql() error {

db, mock, err := sqlmock.New()

if err != nil {

return err

}

defer db.Close()

// ExpectPrepare,期望执行一条Prepare语句 mock.ExpectPrepare("SELECT TIMEDIFF")

mock.ExpectPrepare("SELECT ENGINE")

// ExpectExec,期望执行一条Exec语句 // 然后假定会返回(1, 1),也就是自增主键为1,1条影响结果 mock.ExpectExec("INSERT").

WillReturnResult(sqlmock.NewResult(1, 1))

orm.Debug = true

o, err := orm.NewOrmWithDB("mysql", "default", db)

if err != nil {

return err

}

_ = o.Using("db1")

// beego要求需要先注册结构体 orm.RegisterModel(new(TExchangeInfo))

u := &TExchangeInfo{

ID: 10086,

DeparmentID: 1,

Times: 0,

Number: 10,

}

_, err = o.InsertOrUpdate(u)

return err

}

添加你的期望,然后执行orm动作。接着我们在标准输出口看到打印出来的sql语句

=== RUN TestInsertOrUpdatePrintSql

[ORM]2020/09/16 23:43:39 -[Queries/default] - [ OK / db.Exec / 0.1ms] - [INSERT INTO `t_exchange_info` (`deparment_id`, `times`, `number`, `lastmodified`) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE `deparment_id`=?, `times`=?, `number`=?, `lastmodified`=?] - `1`, `0`, `10`, `2020-09-16 23:43:39.178543 +0800 CST`, `1`, `0`, `10`, `2020-09-16 23:43:39.178543 +0800 CST`

--- PASS: TestInsertOrUpdatePrintSql (0.00s)

PASS

分析问题

整理一下输出语句,我们发现,beego orm使用的是数据库自身的insert or update功能来实现的新增插入修改更新的交互。但是整条语句中却毫无主键的痕迹——

INSERT INTO `t_exchange_info` (`deparment_id`, `times`, `number`, `lastmodified`) VALUES (`1`, `0`, `10`, `2020-09-16 23:43:39.178543 +0800 CST`) ON DUPLICATE KEY UPDATE `deparment_id`=`1`, `times`=`0`, `number`=`10`, `lastmodified`=`2020-09-16 23:43:39.178543 +0800 CST`

那么我们应该意识到,很可能是beego orm在执行过程中,过滤掉了主键。这难道是个bug吗?

在追溯源码之后,我们判定问题在于github.com/astaxie/beego@v1.12.2/orm/db_mysql.go第122行代码这里。快速使用Goland自带的断点debug功能打一个断点,然后进行单步调试。

最终我们发现真正问题在于在github.com/astaxie/beego@v1.12.2/orm/db.go第91行这里,在结构体字段的tag中包含有auto属性时,会被跳过,这就是造成过滤的原因。

结论

经过咨询得知,那位大兄弟在建立数据库交互所使用的数据结构体时,习惯在主键上打一个autotag,认为这样表示主键自增的意思。

我告诉他,auto标签只是用于告诉框架进行自增操作,属于框架代码层面的操作,而不是数据库层面的操作,并不表示为主键。如果要表示主键,也应该是pk。

去掉auto,问题解决。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值