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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 02:55:23