- 单元测试主要是为了保证一个模块内部的逻辑正确性,细分下去就是保证每一个函数,甚至每个函数内部的操作符合要求,逻辑正确。而针对在线项目,基于远程数据库的测试,在实际软件开发的过程中是必要的。在目前开发的过程中,往往会使用各种orm代替原生sqldb使用,而基于这些orm的测试会更方便与开发过程,更容易发现问题,提高效率。
本次介绍基于gorm的mock数据库单元测试,通过检测操作一致性、数据一致性、保证最终单元的逻辑一致性,并使用具体事例介绍测试过程。 - 总的来说,就是
操作一致性、数据一致性 =》 逻辑一致性
1. 使用sqlmock进行基于mysql db的单元测试
- 设置期望逻辑
- 运行模拟测试
- 检查数据一致性
1.1 需要测试的示例代码准备
-
需要进行测试的操作,基于http request的mysql SELECT操作
//mysql db 指针 type api struct { db *sql.DB } //在被测试文件(当前文件)内,定义好了关于数据库的Query操作。 //post 函数通过处理http请求,返回数据库中要求的内容 func (a *api) posts(w http.ResponseWriter, r *http.Request) { //需要测试的函数 a.db.Query() rows, err := a.db.Query("SELECT id, title, index FROM posts") if err != nil { a.fail(w, "failed to fetch posts: "+err.Error(), 500) return } defer rows.Close() var posts []*post for rows.Next() { p := &post{} if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil { a.fail(w, "failed to scan post: "+err.Error(), 500) return } posts = append(posts, p) } if rows.Err() != nil { a.fail(w, "failed to read all posts: "+rows.Err().Error(), 500) return } data := struct { Posts []*post }{posts} a.ok(w, data) }
1.2 测试代码
import “github.com/DATA-DOG/go-sqlmock”
mock测试的使用需要三步:
-
初始化sqlmock
db, mock, err := sqlmock.New() if err != nil { ... } defer db.Close()
-
设置期望逻辑
定义执行的数据库操作、返回row的头和行
rows := sqlmock.NewRows([]string{"id", "title", "body"}). AddRow(1, "post 1", "hello"). AddRow(2, "post 2", "world") //使用正则表达式 mock.ExpectQuery("^SELECT (.*)").WillReturnRows(rows)
这里需要注意,使用sqlmock进行测试的过程和我一开始想象的很不一样…
我以为是自定义初始化数据,然后通过处理后与处理前对比观察,但sqlmock是直接利用正则表达式检查查询语句的逻辑性。返回数据可以自行定义。
-
使用mock的虚拟数据库进行模拟数据库测试:
app := &api{db} app.posts(w, req)//调用被测试函数
-
测试http的w.Body,以及是否json解码正确
if w.Code != 200 {//测试w.code t.Fatalf("expected status code to be 200, but got: %d", w.Code) } data := struct { Posts []*post }{Posts: []*post{ {ID: 1, Title: "post 1", Body: "hello"}, {ID: 2, Title: "post 2", Body: "world"}, }} //测试w.Body的byte是否可以解码成功 app.assertJSON(w.Body.Bytes(), data, t) //确保所有数据库操作按照要求 if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) }
1.3 运行结果
1.4 其他期望设置
mock.ExpectBegin()
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO product_viewers").
WithArgs(2, 3).//数据库操作的运行参数具体数值
WillReturnError(fmt.Errorf("some error"))
mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectRollback()
mock.ExpectCommit()
//每一步操作。包括执行初始begin、操作逻辑正则表达式、参数、返回结果、查询结果、错误、回滚、提交....都会被严格测试到
1.5 sqlstruct的高级结果定义
-
期望查询结果格式 column
columns := []string{“o_id”, “o_status”, “o_value”, “u_id”, “u_balance”}
第一个字符为指定的标记,下划线后面的内容为 sql: “tag”
-
给sql语句设置期望返回
mock.ExpectQuery("SELECT (.+) FROM orders AS o INNER JOIN users AS u (.+) FOR UPDATE"). WithArgs(1). WillReturnRows(sqlmock.NewRows(columns).FromCSVString("1,1,1,1,100")) //"内容分别对应定义的column,即可传递到被测试的函数中"
-
被测试函数接受数据
// 接受以u_开头对应标签的数据给user err = sqlstruct.ScanAliased(&user, rows, "u") if err != nil { tx.Rollback() return }
-
在被测试函数中,根据测试函数对于结果的定义,进行不同操作
-
测试函数对被测试函数的操作进行检测。
使用mock测试SELECT正确性的过程,是根据逻辑运行来测试的。对于结果的测试,是测试函数给被测试函数传递行结果的过程…再进一步观测被测试函数根据这个结果的反映。(有点奇怪但习惯了就好咯~)
2. 基于gorm的mock测试
- 初始化测试器:重点在于使用mock的虚拟db初始化gorm
- 设置期望逻辑
- 运行模拟测试
- 检查数据一致性
2.1 初始化测试器
参考网上的博客,并根据我们后台的情况进行修改,比较符合我们的MVC架构,方便进行数据测试。
- 首先是数据结构,有三个字段:suite.Suite辅助测试、DB保存虚拟gorm数据库接口、mock保存测试接口。
- AfterTest函数为运行go test最后执行,确保全程没有错误。TestInit函数保证在运行go test的时候将测试过程传递给我们自定义的测试结构。
- SetupSuite进行数据库初始化,使用mock产生的虚拟数据库进行gorm的初始化,从而符合我们的后台环境。
2.2 被测试函数样例
- 查询样例——被测试函数
- 修改样例——被测试函数
2.3 测试过程
-
查询样例
过程和之前几乎完全一样,但由于gorm有一些改变:-
逻辑语句的改变:
类似于
tx.Model(u).Where("username = ?", u.Username).Update("icon", u.Icon)
已经封装好的sql语句,不能显示地找到,需要查询官网
http://jinzhu.me/gorm/
-
参数的改变,类似于上面逻辑语句中的
u.Username
u.Icon
参数,需要严格按照sql语句的顺序放置在db.WithArgs("…","…")里面 -
对于error,由于有了suite的改动,可以直接s.T().Error(…)
-
由于我们项目的model结构很规范,可以自己实例化一个user,初始化user的数据,再根据函数返回的结果,进行数据测试。
-
-
修改样例
- 和查询样例几乎相同,不过对于不同的Query要注意不同的ExpectExec…可以查询官方文档了解
- 对于Exec来说,并没有查询结果了,需要提前定义一个result传递进去,用于定义期望执行影响到的行数。
- 涉及到数据的改动,需要注意执行前的Begin和执行后的Commit
2.4 测试出现问题
-
对于逻辑问题
如果上述的修改样例改为SELECT操作,如下
正确应为.Update(…)
会出现以下错误,分别告知期望的和实际的并不匹配。
-
对于参数问题
如果将修改password传递的参数错写为icon
会报错误:Argument do not match -
对于修改后数据问题
自行定义数据测试,关于查询后数据需要和数据库内的数据相符,如果查询后结果与期望不同,则传递错误。
2.4 关于不同结果的测试
可以通过之前介绍的sqlmock的高级结果定义,以及willReturnRows来向被测试函数传递定义好的不同的结果,测试查询后,函数根据不同的查询结果做出的操作是否正确。比如对于查询结果为空、查询状态不对后的回滚操作…等等
3. 附录
gorm 查询对应的底层sql语句:http://jinzhu.me/gorm/
mock-gorm思路:https://medium.com/@rosaniline/unit-testing-gorm-with-go-sqlmock-in-go-93cbce1f6b5b
以及gormdoc、sqlmockdoc、testingdoc