Golang创建一个模拟数据库,使用处理程序和接口调用数据库

jexiocij  于 2023-03-27  发布在  Go
关注(0)|答案(1)|浏览(155)

我试图在我的SignUp处理程序和对数据库的调用上实现单元测试。然而,它在我的SignUp处理程序中的数据库调用上抛出了panic错误。这是一个简单的SignUp处理程序,它接收带有用户名,密码和电子邮件的JSON。然后我将使用SELECT语句来检查此用户名是否在SignUp处理程序本身中重复。
当我把我的post请求发送到这个处理程序时,这一切都可以工作。然而,当我实际进行单元测试时,它不工作,并向我抛出了2条错误消息。我觉得这是因为数据库没有在测试环境中初始化,但我不知道如何在不使用第三方框架进行模拟数据库的情况下做到这一点。

错误信息

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

注册。去

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?

46scxncf

46scxncf1#

让我试着帮助你弄清楚如何实现这些事情。我对你的代码进行了一点重构,但总体思路和使用的工具仍然与你的相同。首先,我将分享分散在两个文件中的生产代码:handlers/handlers.gorepo/repo.go

handlers/handlers.go文件

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一起提供的ctx
1.客户端使用的sql。正确的方法是通过context.Context传递。对于这种情况,您不必构建任何结构或使用任何接口等。只需编写一个期望*sql.DB作为参数的函数。请记住,函数是一等公民
"DB"应该是一个常量,我们必须检查上下文值中是否存在这个条目,但为了简洁起见,我省略了这些检查。

repo/repo.go文件

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的专用方法。
1.当你需要建立一个SQL查询时,使用预准备语句特性。不要用fmt.Sprintf连接东西,原因有两个:安全性和可测试性。
现在,我们来看看测试代码。

handlers/handlers_test.go文件

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)
        }
    })
}

这里,与您的版本相比有很多变化。让我快速回顾一下:

  • 使用子测试功能为测试给予层次结构。
  • 使用httptest包,它提供了构建和AssertHTTP请求和响应的内容。
  • 使用sqlmock包。当涉及到模拟数据库时,它是事实上的标准。
  • 使用contextsql客户端与http.Request并排传递。
  • Assert已经在github.com/stretchr/testify/assert包中完成。

这同样适用于这里:有重构的空间(例如,您可以通过使用表驱动测试特性来返工测试)。

外接

这可以被认为是编写Go代码的惯用方法。我知道这可能非常具有挑战性,特别是在开始的时候。如果你需要某些部分的进一步细节,请告诉我,我很乐意帮助你,谢谢!

相关问题