我正在try 在我的注册处理程序和对数据库的调用上实现单元测试.然而,它在我的注册处理程序中的数据库调用中抛出了死机错误.它是一个简单的注册处理程序,可以接收带有用户名、密码和邮箱的JSON.然后,我将使用SELECT语句判断该用户名在注册处理程序本身中是否重复.

当我向这个处理程序发送我的POST请求时,这一切都起作用.然而,当我实际进行单元测试时,它不起作用,并向我抛出2条错误消息.我认为这是因为数据库没有在测试环境中初始化,但我不确定如何在不使用第三方框架进行模拟数据库的情况下做到这一点.

error message

panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference

signup.go

package handler

type SignUpJson struct {
    Username string `json:"username"`
    Password string `json:"password"`
    Email    string `json:"email"`
}

func SignUp(w http.ResponseWriter, r *http.Request) {
    // Set Headers
    w.Header().Set("Content-Type", "application/json")
    var newUser auth_management.SignUpJson

    // Reading the request body and UnMarshal the body to the LoginJson struct
    bs, _ := io.ReadAll(req.Body)
    if err := json.Unmarshal(bs, &newUser); err != nil {
        utils.ResponseJson(w, http.StatusInternalServerError, "Internal Server Error")
        log.Println("Internal Server Error in UnMarshal JSON body in SignUp route:", err)
        return
    }

    ctx := context.Background()
    ctx, cancel = context.WithTimeout(ctx, time.Minute * 2)
    defer cancel()

    // Check if username already exists in database (duplicates not allowed)
    isExistingUsername := database.GetUsername(ctx, newUser.Username) // throws panic error here when testing
    if isExistingUsername {
        utils.ResponseJson(w, http.StatusBadRequest, "Username has already been taken. Please try again.")
        return
    }

    // other code logic...
}

sqlquery.go

package database

var SQL_SELECT_FROM_USERS = "SELECT %s FROM users WHERE %s = $1;"

func GetUsername(ctx context.Context, username string) bool {
    row := conn.QueryRow(ctx, fmt.Sprintf(SQL_SELECT_FROM_USERS, "username", "username"), username)
    return row.Scan() != pgx.ErrNoRows
}

SignUp_test.go

package handler

func Test_SignUp(t *testing.T) {

    var tests = []struct {
        name               string
        postedData         SignUpJson
        expectedStatusCode int
    }{
        {
            name: "valid login",
            postedData: SignUpJson{
                Username: "testusername",
                Password: "testpassword",
                Email:    "test@email.com",
            },
            expectedStatusCode: 200,
        },
    }

    for _, e := range tests {
        jsonStr, err := json.Marshal(e.postedData)
        if err != nil {
            t.Fatal(err)
        }

        // Setting a request for testing
        req, _ := http.NewRequest(http.MethodPost, "/signup", strings.NewReader(string(jsonStr)))
        req.Header.Set("Content-Type", "application/json")

        // Setting and recording the response
        res := httptest.NewRecorder()
        handler := http.HandlerFunc(SignUp)

        handler.ServeHTTP(res, req)

        if res.Code != e.expectedStatusCode {
            t.Errorf("%s: returned wrong status code; expected %d but got %d", e.name, e.expectedStatusCode, res.Code)
        }
    }
}

setup_test.go

func TestMain(m *testing.M) {

    os.Exit(m.Run())
}

我在这里看到了类似的问题,但不确定这是不是正确的方法,因为没有回应,答案令人困惑:How to write an unit test for a handler that invokes a function that interacts with db in Golang using pgx driver?

推荐答案

让我试着帮助你弄清楚如何实现这些目标.我稍微重构了您的代码,但总体思想和使用的工具仍然与您的相同.首先,我将分享分布在两个文件中的生产代码:handlers/handlers.gorepo/repo.go.

handlers/handlers.go file

package handlers

import (
    "context"
    "database/sql"
    "encoding/json"
    "io"
    "net/http"
    "time"

    "handlertest/repo"
)

type SignUpJson struct {
    Username string `json:"username"`
    Password string `json:"password"`
    Email    string `json:"email"`
}

func SignUp(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    var newUser SignUpJson
    bs, _ := io.ReadAll(r.Body)
    if err := json.Unmarshal(bs, &newUser); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte(err.Error()))
        return
    }

    ctx, cancel := context.WithTimeout(r.Context(), time.Minute*2)
    defer cancel()

    db, _ := ctx.Value("DB").(*sql.DB)
    if isExistingUserName := repo.GetUserName(ctx, db, newUser.Username); isExistingUserName {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("username already present"))
        return
    }
    w.WriteHeader(http.StatusOK)
}

在这里,有两个主要的区别:

  1. 用过的context个.您不必实例化另一个ctx,只需使用http.Request附带的那个即可.
  2. 使用的sql客户端.正确的方法是通过context.Context号公路.对于此场景,您不必构建任何 struct 或使用任何接口等.只需编写一个将*sql.DB作为参数的函数.记住这个,Functions are first-class citizens.

当然,还有重构的空间."DB"应该是一个常量,我们必须判断上下文值中是否存在此条目,但为简洁起见,我省略了这些判断.

repo/repo.go file

package repo

import (
    "context"
    "database/sql"

    "github.com/jackc/pgx/v5"
)

func GetUserName(ctx context.Context, db *sql.DB, username string) bool {
    row := db.QueryRowContext(ctx, "SELECT username FROM users WHERE username = $1", username)
    return row.Scan() != pgx.ErrNoRows
}

这里的代码与您的非常相似,除了这两个小问题:

  1. 当您想要考虑上下文时,有一种称为QueryRowContext的专用方法.
  2. 当您必须构建一个SQL查询时,请使用预准备语句功能.出于两个原因:安全性和可测试性,不要将内容与fmt.Sprintf连接在一起.

现在,我们来看看测试代码.

handlers/handlers_test.go file

package handlers

import (
    "context"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/DATA-DOG/go-sqlmock"
    "github.com/jackc/pgx/v5"
    "github.com/stretchr/testify/assert"
)

func TestSignUp(t *testing.T) {
    db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
    if err != nil {
        t.Fatalf("err not expected while open a mock db, %v", err)
    }
    defer db.Close()
    t.Run("NewUser", func(t *testing.T) {
        mock.ExpectQuery("SELECT username FROM users WHERE username = $1").WithArgs("john.doe@example.com").WillReturnError(pgx.ErrNoRows)

        w := httptest.NewRecorder()
        r := httptest.NewRequest(http.MethodPost, "/signup", strings.NewReader(`{"username": "john.doe@example.com", "password": "1234", "email": "john.doe@example.com"}`))

        ctx := context.WithValue(r.Context(), "DB", db)
        r = r.WithContext(ctx)

        SignUp(w, r)

        assert.Equal(t, http.StatusOK, w.Code)
        if err := mock.ExpectationsWereMet(); err != nil {
            t.Errorf("not all expectations were met: %v", err)
        }
    })

    t.Run("AlreadyExistentUser", func(t *testing.T) {
        rows := sqlmock.NewRows([]string{"username"}).AddRow("john.doe@example.com")
        mock.ExpectQuery("SELECT username FROM users WHERE username = $1").WithArgs("john.doe@example.com").WillReturnRows(rows)

        w := httptest.NewRecorder()
        r := httptest.NewRequest(http.MethodPost, "/signup", strings.NewReader(`{"username": "john.doe@example.com", "password": "1234", "email": "john.doe@example.com"}`))

        ctx := context.WithValue(r.Context(), "DB", db)
        r = r.WithContext(ctx)

        SignUp(w, r)

        assert.Equal(t, http.StatusBadRequest, w.Code)
        if err := mock.ExpectationsWereMet(); err != nil {
            t.Errorf("not all expectations were met: %v", err)
        }
    })
}

在这里,与您的版本相比,有很多变化.让我简单地概括一下:

  • 使用子测试功能为测试提供分层 struct .
  • 使用httptest包,该包提供用于构建和断言HTTP请求和响应的内容.
  • 使用sqlmock套餐.当涉及到嘲笑数据库时,这是事实上的标准.
  • 使用contextsql客户端与http.Request一起传递.
  • 已经对包github.com/stretchr/testify/assert进行了断言.

同样的道理也适用于这里:有重构的空间(例如,您可以通过使用表驱动测试特性来修改测试).

出类拔萃

这可以被认为是编写Go代码的惯用方式.我知道这可能是非常具有挑战性的,特别是在一开始.如果您需要进一步的细节,请让我知道,我很乐意为您服务,谢谢!

Go相关问答推荐

Golang Cososdb-gremlin连接

运行add. inf,这样我们就可以在app.conf中使用. inf参数了?

如何在另一个文件夹中使用Delve运行二进制文件?

Kafka消费者在需要时不会暂停

Golang SDK for DynamoDB:ReturnValuesOnConditionCheckFailure在条件chcek失败发生时不返回条件的详细信息

如何使redis池的等待超时

日志(log)文件不在 golang 的日志(log)目录中

Golang 发送Post请求出现400错误

Kubo,来自 IpfsNode.Bootstrap 的无效内存地址或零指针取消引用

xml.Unmarshal 不支持的类型 struct

当填充通道的函数调用未嵌入 goroutine 时,为什么我会遇到死锁?

设置 graphql 的最大文件上传大小(golang)

Go:如何在将 float64 转换为 float32 时判断精度损失

go-echo中如何防止+转义

如何仅提取时间作为持续时间

Golang Gin 绑定请求正文 XML 到 Slice

使用 `didip/tollbooth` 限制每小时最大请求数

如何获取多个 url 参数值

将接口方法的参数限制为几个允许的 struct ?

Go 语言的select语句