You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 22:58:14