gorm/gen单元测试,基于sqlmock

905 阅读4分钟

测试类型

参考原文:软件测试分哪几种,它们的区别有哪些?

单元测试

  1. 编写单元测试文件应与需要被测试文件的同包以"_test"结尾
  2. 单元测试内的函数应以Test开始以被测试的函数名结尾,且被测试函数名首字母应大写
  3. 测试单元可为简单函数或模块函数
  4. 单元测试应保证分支独立、全面覆盖,测试单元密度足够小,函数单一职责
  5. 组成部分:输入、输出、测试单元、与期望的校对,测试单元又包括函数、接口、模块、复杂的聚合函数等。通过单元测试的输出再与期望输出进行校对,来验证代码的正确性。通过单元测试可以保证代码的质量,也可以在一定程度上提升效率,比如通过运行单元测试可以快速定位到有问题的代码。

以下为一个add函数的测试单元

// my_test.go
func TestAdd(t *testing.T) {
   ans := add(12, 34)
   exp := 46
   if ans != exp {
      t.Error("答案错误")
   }
}

使用assert可简化为

func TestAdd(t *testing.T) {
   assert.Equal(t, 46,add(12, 34), "答案错误")
}

Mock测试

mock测试目标是让用户在单元测试中低成本的完成打桩,mock通过"虚拟"代码替换掉依赖的方法和资源,减少对网络、文件或者数据库的依赖。

使用go-sqlmock对数据库进行Mock测试

核心目的

  • 减小测试过程中对于数据库的依赖
  • 使在测试中产生的数据内容不会对数据库产生影响
  • 对于复杂逻辑应多进行测试,多编写测试样例保证函数中每一条sql、每一个if-else都能被执行,简单逻辑编写少量测试样例即可。

下载安装

go get github.com/DATA-DOG/go-sqlmock

mock数据库初始化

// roleServiceImpl_test.go
// ...
var (  
    mock sqlmock.Sqlmock  
    mockDB *sql.DB  
)  
  
func init() {  
    // 把匹配器设置成相等匹配器,不设置默认使用正则匹配  
    mockDB, mock, _ = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))  
    // 配置mock数据库
    gormDB, _ := gorm.Open(mysql.New(mysql.Config{  
        SkipInitializeWithVersion: true,  
        Conn: mockDB,  
    }), &gorm.Config{})  
    // 设置gen  
    gen.SetDefault(gormDB)  
    fmt.Println("mock数据库已连接")  
}

使用goland生成测试模板

在需要测试的方法内部右键-生成/Generate 选择生成函数测试

image.png

生成的模板结构

func TestRoleServiceImpl_GetRole(t *testing.T) {  
    // 待测试函数所需传入的参数
    type args struct {  
        id int64  
    }  
    // 全部测试样例点 一个测试函数可能需要多个样例点
    tests := []struct {  
        name string  
        args args  
        want *model.Role  
        want1 *errno.Errno  
    }{  
        // TODO: Add test cases. 样例点需要自己编写
    }  
    // 遍历全部样例点
    for _, tt := range tests {  
        // 执行其中一个样例
        t.Run(tt.name, func(t *testing.T) {  
            ro := RoleServiceImpl{}  
            // 执行待测试的函数
            got, got1 := ro.GetRole(tt.args.id)  
            // 断言返回值是否与期望的返回值相同
            assert.Equalf(t, tt.want, got, "GetRole(%v)", tt.args.id)  
            assert.Equalf(t, tt.want1, got1, "GetRole(%v)", tt.args.id)  
        })  
    }  
}

编写测试样例

tests := []struct {  
    name string  
    args args  
    want *model.Role  
    want1 *errno.Errno  
}{  
    {  
        name: "获取学生角色测试点",  // 样例点名称
        args: args{1},  // 样例点传入的函数参数
        want: &model.Role{  // 期望的返回值
            ID: 1,  
            Name: "学生",  
            IsDelete: 0,  
        },  
        want1: nil,   // 期望的返回值
        }, {  
        name: "获取专业管理员角色测试点",  
        args: args{3},  
        want: &model.Role{  
            ID: 3,  
            Name: "专业管理员",  
            IsDelete: 0,  
        },  
        want1: nil,  
    },  
}

获取待替换的sql语句

方法:使用sqlmock替换一条空sql,然后选择一个一个样例点执行,根据报错信息找的需要替换的sql

for _, tt := range tests {  
    t.Run(tt.name, func(t *testing.T) {  
        // 替换空sql
        mock.ExpectQuery("")  
        
        ro := RoleServiceImpl{}  
        got, got1 := ro.GetRole(tt.args.id)  
        assert.Equalf(t, tt.want, got, "GetRole(%v)", tt.args.id)  
        assert.Equalf(t, tt.want1, got1, "GetRole(%v)", tt.args.id)  
    })  
}

执行一个样例

image.png

查看报错信息

image.png

可得知所需替换的sql为 SELECT * FROM `role` WHERE `role`.`id` = ? AND `role`.`is_delete` = ? LIMIT 1 最后再修改测试代码

for _, tt := range tests {  
    t.Run(tt.name, func(t *testing.T) {  
        mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` = ? AND `role`.`is_delete` = ? LIMIT 1")  
        
        ro := RoleServiceImpl{}  
        got, got1 := ro.GetRole(tt.args.id)  
        assert.Equalf(t, tt.want, got, "GetRole(%v)", tt.args.id)  
        assert.Equalf(t, tt.want1, got1, "GetRole(%v)", tt.args.id)  
    })  
}

补充所需替换的数据

for _, tt := range tests {  
    t.Run(tt.name, func(t *testing.T) {  
        mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` = ? AND `role`.`is_delete` = ? LIMIT 1").  
            WithArgs(tt.args.id, 0).  // 补充占位符缺失的数据
            WillReturnRows(  
                sqlmock.NewRows([]string{"id", "name", "is_delete"}).  // 补充sql返回的字段
                AddRow(tt.want.ID, tt.want.Name, tt.want.IsDelete))  // 补充字段内容
        got, got1 := GetUserService().GetRole(tt.args.id)  
        assert.Equalf(t, tt.want, got, "GetRole(%v)", tt.args.id)  
        assert.Equalf(t, tt.want1, got1, "GetRole(%v)", tt.args.id)  
    })  
}

执行样例查看信息

image.png

对于查询列表的测试

没有找到好的处理方法,只能对数据进行分页处理,再进行测试

func TestRoleServiceImpl_GetRoleList(t *testing.T) {  
    tests := []struct {  
        name string  
        want *[]*model.Role  
        want1 *errno.Errno  
    }{  
        {  
            name: "获取角色列表测试点",  
            want: &[]*model.Role{  
                {ID: 1, Name: "学生", IsDelete: 0},  
                {ID: 2, Name: "教师", IsDelete: 0},  
                {ID: 3, Name: "专业管理员", IsDelete: 0},  
                {ID: 4, Name: "院管理员", IsDelete: 0},  
                {ID: 5, Name: "校管理员", IsDelete: 0},  
                {ID: 6, Name: "管理员", IsDelete: 0},  
                {ID: 7, Name: "超级管理员", IsDelete: 0},  
            },  
            want1: nil,  
        },  
    }  
    for _, tt := range tests {  
        t.Run(tt.name, func(t *testing.T) {  
            // 匹配查询sql 并模拟查询到的数据  
            mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`is_delete` = ?").  
                WithArgs(0).  
                WillReturnRows(  
                    sqlmock.NewRows([]string{"id", "name", "is_delete"}).  
                    AddRow((*tt.want)[0].ID, (*tt.want)[0].Name, (*tt.want)[0].IsDelete).  
                    AddRow((*tt.want)[1].ID, (*tt.want)[1].Name, (*tt.want)[1].IsDelete).  
                    AddRow((*tt.want)[2].ID, (*tt.want)[2].Name, (*tt.want)[2].IsDelete).  
                    AddRow((*tt.want)[3].ID, (*tt.want)[3].Name, (*tt.want)[3].IsDelete).  
                    AddRow((*tt.want)[4].ID, (*tt.want)[4].Name, (*tt.want)[4].IsDelete).  
                    AddRow((*tt.want)[5].ID, (*tt.want)[5].Name, (*tt.want)[5].IsDelete).  
                    AddRow((*tt.want)[6].ID, (*tt.want)[6].Name, (*tt.want)[6].IsDelete))  
            ro := RoleServiceImpl{}  
            got, got1 := ro.GetRoleList()  
            assert.Equalf(t, tt.want, got, "GetRoleList()")  
            assert.Equalf(t, tt.want1, got1, "GetRoleList()")  
        })  
    }  
}

增删改的mock测试

应使用以下方法进行测试,update,delete同理

for _, tt := range tests {  
    t.Run(tt.name, func(t *testing.T) {  
        mock.ExpectBegin()  
        mock.ExpectExec("INSERT INTO `role` (`name`) VALUES (?) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`)").  
            WithArgs(tt.args.role.Name).  
            WillReturnResult(sqlmock.NewResult(1, 1))  
        mock.ExpectCommit()  
        ro := RoleServiceImpl{}  
        assert.Equalf(t, tt.want, ro.AddRole(tt.args.role), "AddRole(%v)", tt.args.role)  
    })  
}

报错集合与解决方法

参考文章

blog.csdn.net/EDDYCJY/art…

juejin.cn/post/718918…

juejin.cn/post/718918…

zhuanlan.zhihu.com/p/423482892