GORM插入列顺序不稳定致go-sqlmock测试失败,如何处理?
解决GORM批量插入列顺序不一致导致的SQLMock测试失败问题
问题场景
我定义了如下GORM模型:
type ID uint64 type BaseModel struct { ID ID `gorm:"column:id;primaryKey;autoIncrement" json:"id"` UpdateDate time.Time `gorm:"column:update_date;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" json:"update_date"` CreateDate time.Time `gorm:"column:create_date;default:CURRENT_TIMESTAMP" json:"create_date"` } type Rollback struct { BaseModel PID ID `gorm:"index"` Table string `gorm:"column:tbl_name"` RollbackRow string `gorm:"type:longtext"` }
使用gorm.DB.CreateInBatches执行批量插入操作,并用go-sqlmock编写单元测试,测试函数如下:
func expectRollbackInsert(mock sqlmock.Sqlmock, tablename []string) { args := make([]driver.Value, 0) for _, val := range tablename { args = append(args, 1, val, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()) } mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `rollback` (`payment_id`,`tbl_name`,`rollback_row`,`update_date`,`create_date`) VALUES (?,?,?,?,?)")). WithArgs(args...). WillReturnResult(sqlmock.NewResult(int64(len(tablename)), int64(len(tablename)))) }
测试用例偶尔会因为create_date和update_date的列顺序不一致而失败,某次失败信息如下:
ExecQuery: could not match actual sql: "INSERT INTO `rollback` (`pid`,`tbl_name`,`rollback_row`,`create_date`,`update_date`) VALUES (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?)" with expected regexp "INSERT INTO `rollback` \(`pid`,`tbl_name`,`rollback_row`,`update_date`,`create_date`\) VALUES \(\?,\?,\?,\?,\?\)"
业务场景中插入列的顺序无关紧要,需要调整测试逻辑覆盖所有可能的列顺序情况。
可行解决方案
1. 编写宽松的正则表达式匹配两种列顺序
放弃严格匹配列顺序,改用正则表达式同时匹配update_date在前或create_date在前的情况:
func expectRollbackInsert(mock sqlmock.Sqlmock, tablename []string) { args := make([]driver.Value, 0) for _, val := range tablename { args = append(args, 1, val, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()) } // 匹配两种列顺序的正则 insertRegex := regexp.MustCompile(`INSERT INTO \`rollback\` \(\`pid\`,\`tbl_name\`,\`rollback_row\`,(?:\`update_date\`,\`create_date\`|\`create_date\`,\`update_date\`)\) VALUES (\(\?,\?,\?,\?,\?\),?)+`) mock.ExpectExec(insertRegex). WithArgs(args...). WillReturnResult(sqlmock.NewResult(int64(len(tablename)), int64(len(tablename)))) }
这里用(?:A|B)非捕获组同时匹配两种列顺序组合,(\(\?,\?,\?,\?,\?\),?)+匹配批量插入的多组值占位符。
2. 业务代码中固定插入字段顺序
在调用CreateInBatches时,通过Select方法明确指定要插入的字段顺序,让GORM生成固定顺序的SQL:
// 业务代码中指定插入字段顺序 err := db.Select("pid", "tbl_name", "rollback_row", "create_date", "update_date").CreateInBatches(rollbackList, batchSize).Error
这样测试时就可以用固定的列顺序编写预期SQL,无需处理顺序变化的问题。
3. 自定义SQL匹配逻辑忽略列顺序
如果不想修改正则或业务代码,可以自定义SQL匹配函数,仅验证必要列是否存在、占位符数量是否正确,完全不关心列顺序:
func expectRollbackInsert(mock sqlmock.Sqlmock, tablename []string) { args := make([]driver.Value, 0) for _, val := range tablename { args = append(args, 1, val, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()) } mock.ExpectExec(func(query string) bool { // 校验是否为rollback表的插入语句 if !strings.HasPrefix(query, "INSERT INTO `rollback` (") { return false } // 提取列名部分 colsSegment := strings.TrimPrefix(query, "INSERT INTO `rollback` (") colsSegment = strings.Split(colsSegment, ") VALUES")[0] cols := strings.Split(colsSegment, "`,`") // 去除列名前后的反引号 for i := range cols { cols[i] = strings.Trim(cols[i], "`") } // 验证所有必要列都存在 requiredCols := map[string]bool{"pid": true, "tbl_name": true, "rollback_row": true, "create_date": true, "update_date": true} for col := range requiredCols { found := false for _, c := range cols { if c == col { found = true break } } if !found { return false } } // 验证占位符数量是否符合批量插入的预期 valsSegment := strings.Split(query, " VALUES ")[1] return strings.Count(valsSegment, "?") == len(tablename)*5 }).WithArgs(args...).WillReturnResult(sqlmock.NewResult(int64(len(tablename)), int64(len(tablename)))) }
这种方式灵活性最高,完全不依赖列的顺序,只保证必要条件满足即可。
内容的提问来源于stack exchange,提问作者doptimusprime
相关产品推荐
相关产品推荐

