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

Gin控制器单元测试:调用GetDivisions时触发空指针解引用Panic的问题排查

解决Gin控制器GetDivisions单元测试的空指针Panic问题

首先明确回答你的疑问:把gin.Context作为参数传递给GetDivisions是完全没问题的,这正是Gin控制器单元测试的标准写法,你的用法本身是正确的。

那为什么会触发空指针panic呢?咱们看一下你的GetDivisions实现:

func GetDivisions(c *gin.Context) {
    var divisions []models.Division
    if err := config.DB.Find(&divisions).Error; err != nil {
        c.JSON(http.StatusInternalServerError, err.Error())
        return
    }
    // ...
}

这里的config.DB在测试运行时是nil状态——你在业务代码里可能在程序启动时初始化了数据库连接,但测试环境不会自动执行那部分初始化逻辑,所以调用config.DB.Find()时,相当于对一个nil指针调用方法,直接触发了空指针异常。

接下来给你两种解决方案,根据你的测试需求选择:

方案1:初始化测试专用数据库(集成测试)

如果需要用真实数据库验证逻辑,可以在测试函数开头初始化一个测试用的DB(比如内存SQLite,不会污染生产数据):

func TestGetDivisions(t *testing.T) {
    // 初始化内存SQLite数据库
    var err error
    config.DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        t.Fatalf("初始化测试数据库失败: %v", err)
    }
    // 自动迁移Division模型到测试DB
    err = config.DB.AutoMigrate(&models.Division{})
    if err != nil {
        t.Fatalf("迁移测试数据库表失败: %v", err)
    }

    // 测试案例定义
    tt := []struct {
        Name          string
        EnterpriseID  string
        Code          int
    }{
        {Name: "存在的企业ID", EnterpriseID: "1", Code: 200},
        {Name: "非法字符串企业ID", EnterpriseID: "string", Code: 404},
    }

    for _, tc := range tt {
        // 每个测试案例创建独立的上下文和响应记录器,避免状态污染
        w := httptest.NewRecorder()
        c, _ := gin.CreateTestContext(w)
        c.Params = []gin.Param{
            {Key: "enterprise_id", Value: tc.EnterpriseID},
        }

        GetDivisions(c)
        assert.Equal(t, tc.Code, w.Code, tc.Name)
    }
}

方案2:Mock数据库操作(纯单元测试,更推荐)

单元测试的核心是隔离依赖,所以我们可以用mock库模拟数据库的Find操作,不需要真实数据库,测试速度更快也更稳定:

首先安装mock库:

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

然后修改测试代码:

func TestGetDivisions(t *testing.T) {
    // 创建mock的SQL连接和sqlmock实例
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("创建mock数据库失败: %v", err)
    }
    defer db.Close()

    // 将mock连接包装为GORM的DB实例
    gormDB, err := gorm.Open(sqlite.Dialector{Conn: db}, &gorm.Config{})
    if err != nil {
        t.Fatalf("将mock连接转为GORM DB失败: %v", err)
    }
    // 替换全局的config.DB为mock实例
    config.DB = gormDB

    // 测试案例,新增mockSetup字段定义每个案例的数据库行为
    tt := []struct {
        Name          string
        EnterpriseID  string
        Code          int
        mockSetup     func()
    }{
        {
            Name:          "存在的企业ID",
            EnterpriseID:  "1",
            Code:          200,
            mockSetup: func() {
                // 模拟Find操作返回一条匹配的部门数据
                rows := sqlmock.NewRows([]string{"id", "name", "enterprise_id"}).
                    AddRow(1, "技术部", "1")
                mock.ExpectQuery("SELECT \\* FROM \"divisions\"").WillReturnRows(rows)
            },
        },
        {
            Name:          "非法字符串企业ID",
            EnterpriseID:  "string",
            Code:          404,
            mockSetup: func() {
                // 模拟Find操作返回空结果(无匹配数据)
                rows := sqlmock.NewRows([]string{"id", "name", "enterprise_id"})
                mock.ExpectQuery("SELECT \\* FROM \"divisions\"").WillReturnRows(rows)
            },
        },
    }

    // 使用t.Run创建子测试,每个案例独立运行
    for _, tc := range tt {
        t.Run(tc.Name, func(t *testing.T) {
            w := httptest.NewRecorder()
            c, _ := gin.CreateTestContext(w)
            c.Params = []gin.Param{
                {Key: "enterprise_id", Value: tc.EnterpriseID},
            }

            // 执行当前案例的mock设置
            tc.mockSetup()

            GetDivisions(c)
            assert.Equal(t, tc.Code, w.Code)

            // 验证所有mock的期望都被执行了
            if err := mock.ExpectationsWereMet(); err != nil {
                t.Errorf("未满足mock期望: %v", err)
            }
        })
    }
}

额外优化点

你原来的测试代码里复用了同一个w和c,这会导致前一个测试案例的响应状态码残留,影响下一个案例的结果。上面的两种方案都把上下文和响应记录器的创建放到了循环/子测试内部,保证每个案例的独立性。

内容的提问来源于stack exchange,提问作者richxcame

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 22:42:44