前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Go系列:这个单测为什么过不了

Go系列:这个单测为什么过不了

原创
作者头像
用户9805946
修改2024-11-21 14:29:26
修改2024-11-21 14:29:26
490
举报
文章被收录于专栏:Go

背景

最近工作中需要写mysql相关单测,但是有个case一直报错,请看如下示意代码

user的model定义代码,包括user结构定义和一个ListUser方法package models

代码语言:bash
复制
import "gorm.io/gorm"

type User struct {
	Id    int    `gorm:"id"`
	Name  string `gorm:"name"`
	Email string `gorm:"email"`
}

func (User) TableName() string {
	return "users"
}

func ListUsers(db \*gorm.DB) ([]User, error) {
	var users []User
	err := db.Find(&users).Error
	return users, err
}
- 下面对ListUsers写一个单测package models

import (
	"database/sql"
	"strconv"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var (
	sqlDB   \*sql.DB
	sqlMock sqlmock.Sqlmock
	gormDB  \*gorm.DB
)

func setupTestCase(t \*testing.T) func(t \*testing.T) {

	var err error
	// gorm
	sqlDB, sqlMock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatal(err)
	}
	gormDB, err = gorm.Open(mysql.New(mysql.Config{
		SkipInitializeWithVersion: true,
		Conn:                      sqlDB,
	}), &gorm.Config{})
	if err != nil {
		t.Fatal(err)
	}

	return func(t \*testing.T) {
		\_ = sqlDB.Close()
	}
}

func TestListUsers(t \*testing.T) {

	rows := sqlmock.NewRows([]string{"id", "name", "email"}).AddRow("1", "test", "test@test.com")

	for i := 0; i < 2; i++ {
		t.Run(strconv.Itoa(i), func(t \*testing.T) {
			teardownTestCase := setupTestCase(t)
			defer teardownTestCase(t)

			sqlMock.ExpectQuery("^SELECT (.+) FROM `users`").WillReturnRows(rows)
			users, \_ := ListUsers(gormDB)
			assert.Equal(t, 1, len(users))
		})
	}

}

单测总体也比较简单,主要看TestListUsers方法,在这个方法里

  • 定义了一个查询SQL的返回结果
  • 定义了一个重复2次的循环
  • 在每个循环里,创建sql.DB, sqlmocck, gorm.DB
  • 对查询进行mock
  • 执行ListUsers方法
  • 检查返回结果==1

由于我们的ListUsers就是一个简单的Selct,所以ExpectQuery一定匹配上

预期在2个循环周期内,ListUsers都返回第一步定位的rows,断言成功

但是实际上,第一次断言成功,第二次失败

代码语言:bash
复制
/usr/local/go1.22.3/bin/go tool test2json -t ... -test.run ^\QTestListUsers\E$
=== RUN   TestListUsers
=== RUN   TestListUsers/0
--- PASS: TestListUsers/0 (0.00s)
=== RUN   TestListUsers/1
    user_test.go:52: 
        	Error Trace:	../models/user_test.go:52
        	Error:      	Not equal: 
        	            	expected: 1
        	            	actual  : 0
        	Test:       	TestListUsers/1
--- FAIL: TestListUsers/1 (0.00s)

预期:1
实际:0

如何修改

为每次ExepectQuery都创建单独的rows即可

TestListUsers修改如下

代码语言:go
复制
func TestListUsers(t *testing.T) {

	for i := 0; i < 2; i++ {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			teardownTestCase := setupTestCase(t)
			defer teardownTestCase(t)

			rows := sqlmock.NewRows([]string{"id", "name", "email"}).AddRow("1", "test", "test@test.com")
			sqlMock.ExpectQuery("^SELECT (.+) FROM `users`").WillReturnRows(rows)
			users, _ := ListUsers(gormDB)
			assert.Equal(t, 1, len(users))
		})
	}
}

原因

首先我们来看sqlmock.Row的结构体

代码语言:go
复制
// Rows is a mocked collection of rows to
// return for Query result
type Rows struct {
	converter driver.ValueConverter
	cols      []string
	def       []*Column
	rows      [][]driver.Value
	pos       int
	nextErr   map[int]error
	closeErr  error
}

发现这个结构体里有个pos字段,那是不是因为第一次读取后,修改了pos字段,导致第二次读取的时候就读不到内容呢,我们继续看

Rows结构体有个Next方法,这个看接口说明就是用来读取数据的,然后我们看到这里面的确是会修改pos

代码语言:go
复制
// advances to next row
func (rs *rowSets) Next(dest []driver.Value) error {
	r := rs.sets[rs.pos]
	r.pos++
	rs.invalidateRaw()
	if r.pos > len(r.rows) {
		return io.EOF // per interface spec
	}

	for i, col := range r.rows[r.pos-1] {
		if b, ok := rawBytes(col); ok {
			rs.raw = append(rs.raw, b)
			dest[i] = b
			continue
		}
		dest[i] = col
	}

	return r.nextErr[r.pos-1]
}

这个函数的大概逻辑就是根据pos的位置,返回rows里的row

结论

sqlmock.Rows对象不要复用,每次都需要重现创建一个

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 如何修改
  • 原因
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档