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
相关产品推荐
相关产品推荐

