Golang无第三方框架实现SignUp Handler单元测试Mock数据库问题
解决SignUp Handler单元测试中的数据库空指针Panic问题
问题背景
SignUp Handler在生产环境中可正常处理包含username、password、email的JSON请求,通过SELECT语句检查用户名重复,但单元测试时触发空指针解引用Panic,核心原因是测试环境未初始化数据库连接,sqlquery.go中的GetUsername函数调用了未初始化的conn变量。
错误信息:
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
无第三方框架的Mock实现方案
1. 抽象数据库操作接口
先把数据库相关逻辑封装成接口,让业务代码依赖接口而非具体的数据库连接实例:
// sqlquery.go type DBQuerier interface { GetUsername(username string) (bool, error) // 可按需添加其他数据库操作方法,比如创建用户 } // 适配接口的真实数据库连接实现 type SQLConn struct { conn *sql.DB } func (s *SQLConn) GetUsername(username string) (bool, error) { var exists bool err := s.conn.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)", username).Scan(&exists) return exists, err }
2. 修改Handler依赖接口
调整SignUp Handler,使其接收DBQuerier接口作为参数,解耦与具体数据库的绑定:
// signup.go func SignUpHandler(w http.ResponseWriter, r *http.Request, db DBQuerier) { // 解析请求体逻辑 var reqBody struct { Username string `json:"username"` Password string `json:"password"` Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { http.Error(w, "invalid request", http.StatusBadRequest) return } exists, err := db.GetUsername(reqBody.Username) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if exists { http.Error(w, "username already exists", http.StatusConflict) return } // 后续创建用户逻辑 } // 生产环境初始化真实数据库连接的Handler func NewSignUpHandler() http.HandlerFunc { dbConn, _ := sql.Open("mysql", "your-dsn-string") // 实际初始化逻辑 sqlConn := &SQLConn{conn: dbConn} return func(w http.ResponseWriter, r *http.Request) { SignUpHandler(w, r, sqlConn) } }
3. 手写Mock实现用于测试
在测试文件中实现DBQuerier接口的Mock版本,模拟不同场景的数据库返回:
// SignUp_test.go type MockDBQuerier struct { GetUsernameFunc func(username string) (bool, error) } func (m *MockDBQuerier) GetUsername(username string) (bool, error) { if m.GetUsernameFunc != nil { return m.GetUsernameFunc(username) } return false, nil } // 测试用户名已存在场景 func TestSignUp_UsernameExists(t *testing.T) { mockDB := &MockDBQuerier{ GetUsernameFunc: func(username string) (bool, error) { return true, nil // 模拟用户名已存在 }, } req, _ := http.NewRequest("POST", "/signup", strings.NewReader(`{"username":"test","password":"123","email":"test@example.com"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() SignUpHandler(w, req, mockDB) if w.Code != http.StatusConflict { t.Errorf("expected status 409, got %d", w.Code) } } // 测试用户名可用场景 func TestSignUp_UsernameAvailable(t *testing.T) { mockDB := &MockDBQuerier{ GetUsernameFunc: func(username string) (bool, error) { return false, nil // 模拟用户名不存在 }, } req, _ := http.NewRequest("POST", "/signup", strings.NewReader(`{"username":"newuser","password":"123","email":"new@example.com"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() SignUpHandler(w, req, mockDB) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } }
4. 测试环境无需真实数据库
通过接口抽象和Mock实现,测试时完全不需要连接真实数据库,所有数据库操作的返回结果由Mock函数控制,彻底避免空指针Panic。
总结
通过接口抽象解耦业务逻辑与数据库实现,手写Mock结构体模拟数据库行为,无需依赖第三方框架即可安全完成SignUp Handler的单元测试,同时解决空指针问题。
内容的提问来源于stack exchange,提问作者Jessica
相关产品推荐
相关产品推荐

