MOCK测试:基于gorm的mock单元测试

  • 单元测试主要是为了保证一个模块内部的逻辑正确性,细分下去就是保证每一个函数,甚至每个函数内部的操作符合要求,逻辑正确。而针对在线项目,基于远程数据库的测试,在实际软件开发的过程中是必要的。在目前开发的过程中,往往会使用各种orm代替原生sqldb使用,而基于这些orm的测试会更方便与开发过程,更容易发现问题,提高效率。
    本次介绍基于gorm的mock数据库单元测试,通过检测操作一致性、数据一致性、保证最终单元的逻辑一致性,并使用具体事例介绍测试过程。
  • 总的来说,就是
    操作一致性、数据一致性 =》 逻辑一致性

1. 使用sqlmock进行基于mysql db的单元测试

  • 设置期望逻辑
  • 运行模拟测试
  • 检查数据一致性

1.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测试的使用需要三步:

  1. 初始化sqlmock

    db, mock, err := sqlmock.New()
    	if err != nil {
    		...
    	}
    	defer db.Close()
    
  2. 设置期望逻辑

    定义执行的数据库操作、返回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是直接利用正则表达式检查查询语句的逻辑性。返回数据可以自行定义。

  3. 使用mock的虚拟数据库进行模拟数据库测试:

    app := &api{db}
    app.posts(w, req)//调用被测试函数
    
  4. 测试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的高级结果定义

  1. 期望查询结果格式 column

    columns := []string{“o_id”, “o_status”, “o_value”, “u_id”, “u_balance”}

    第一个字符为指定的标记,下划线后面的内容为 sql: “tag”

  2. 给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,即可传递到被测试的函数中"
    
  3. 被测试函数接受数据

    //                         接受以u_开头对应标签的数据给user
    err = sqlstruct.ScanAliased(&user, rows, "u")
    	if err != nil {
    		tx.Rollback()
    		return
    	}
    
  4. 在被测试函数中,根据测试函数对于结果的定义,进行不同操作

  5. 测试函数对被测试函数的操作进行检测。

使用mock测试SELECT正确性的过程,是根据逻辑运行来测试的。对于结果的测试,是测试函数给被测试函数传递行结果的过程…再进一步观测被测试函数根据这个结果的反映。(有点奇怪但习惯了就好咯~)

2. 基于gorm的mock测试

  • 初始化测试器:重点在于使用mock的虚拟db初始化gorm
  • 设置期望逻辑
  • 运行模拟测试
  • 检查数据一致性

2.1 初始化测试器

在这里插入图片描述

参考网上的博客,并根据我们后台的情况进行修改,比较符合我们的MVC架构,方便进行数据测试。

  1. 首先是数据结构,有三个字段:suite.Suite辅助测试、DB保存虚拟gorm数据库接口、mock保存测试接口。
  2. AfterTest函数为运行go test最后执行,确保全程没有错误。TestInit函数保证在运行go test的时候将测试过程传递给我们自定义的测试结构。
  3. SetupSuite进行数据库初始化,使用mock产生的虚拟数据库进行gorm的初始化,从而符合我们的后台环境。

2.2 被测试函数样例

  1. 查询样例——被测试函数
    在这里插入图片描述
  2. 修改样例——被测试函数
    在这里插入图片描述

2.3 测试过程

  1. 查询样例
    在这里插入图片描述
    过程和之前几乎完全一样,但由于gorm有一些改变:

    1. 逻辑语句的改变:

      类似于

      tx.Model(u).Where("username = ?", u.Username).Update("icon", u.Icon)

      已经封装好的sql语句,不能显示地找到,需要查询官网

      http://jinzhu.me/gorm/

    2. 参数的改变,类似于上面逻辑语句中的u.Username u.Icon参数,需要严格按照sql语句的顺序放置在db.WithArgs("…","…")里面

    3. 对于error,由于有了suite的改动,可以直接s.T().Error(…)

    4. 由于我们项目的model结构很规范,可以自己实例化一个user,初始化user的数据,再根据函数返回的结果,进行数据测试。

  2. 修改样例
    在这里插入图片描述

    • 和查询样例几乎相同,不过对于不同的Query要注意不同的ExpectExec…可以查询官方文档了解
    • 对于Exec来说,并没有查询结果了,需要提前定义一个result传递进去,用于定义期望执行影响到的行数。
    • 涉及到数据的改动,需要注意执行前的Begin和执行后的Commit

2.4 测试出现问题

  1. 对于逻辑问题

    如果上述的修改样例改为SELECT操作,如下
    在这里插入图片描述

    正确应为.Update(…)

    会出现以下错误,分别告知期望的和实际的并不匹配。
    在这里插入图片描述
    在这里插入图片描述

  2. 对于参数问题

    如果将修改password传递的参数错写为icon 在这里插入图片描述
    会报错误:Argument do not match 在这里插入图片描述

  3. 对于修改后数据问题

在这里插入图片描述
自行定义数据测试,关于查询后数据需要和数据库内的数据相符,如果查询后结果与期望不同,则传递错误。

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

发布了19 篇原创文章 · 获赞 8 · 访问量 5994

猜你喜欢

转载自blog.csdn.net/forevermoonlight/article/details/102943375