如何使用 Gorm、mux、postgresql 进行单元测试

发布于 2025-01-17 17:42:02 字数 4749 浏览 0 评论 0原文

我是 Go 和单元测试的新手。我使用 Go 与 Gorm、mux 和 postgresql 构建了一个名为“urlshortener”的小型项目。

搜索了很多文章后,有一个问题让我很烦恼。

为了使问题更清晰,我删除了一些不相关的代码,例如 connect db、.env 等。

我的代码如下(main.go):

package main

type Url struct {
    ID       uint   `gorm:"primaryKey"` // used for shortUrl index
    Url      string `gorm:"unique"`     // prevent duplicate url
    ExpireAt string
    ShortUrl string
}

var db *gorm.DB
var err error


func main() {
    // gain access to database by getting .env
    ...

    // database connection string
    ...

    // make migrations to the dbif they have not already been created
    db.AutoMigrate(&Url{})

    // API routes
    router := mux.NewRouter()

    router.HandleFunc("/{id}", getURL).Methods("GET")

    router.HandleFunc("/api/v1/urls", createURL).Methods("POST")
    router.HandleFunc("/create/urls", createURLs).Methods("POST")

    // Listener
    http.ListenAndServe(":80", router)

    // close connection to db when main func finishes
    defer db.Close()
}

现在我正在为 getURL 函数构建单元测试,这是一个从我的数据库获取数据的 GET 方法。 postgresql 数据库名为 urlshortener,表名为 urls。

这是 getURL 函数代码:

func getURL(w http.ResponseWriter, r *http.Request) {
    params := mux.Vars(r)
    var url Url

    err := db.Find(&url, params["id"]).Error
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
    } else {
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(url.Url)
    }
}

这对于我的数据库工作得很好。请参阅下面的curl命令: 输入图片这里的描述

我知道单元测试不是针对模拟数据的,它的目的是测试一个函数/方法是否稳定。虽然我导入了 mux 和 net/http 进行连接,但我认为对其进行的单元测试应该是“SQL 语法”。因此,我决定重点测试 gorm 是否向测试函数返回正确的值。

在这种情况下,db.Find 将返回一个 *gorm.DB 结构,该结构应与第二行完全相同。 (请参阅文档https://gorm.io/docs/query.html

db.Find(&url, params["id"])
SELECT * FROM urls WHICH id=<input_number>

我的问题是在这种情况下(gorm+mux)如何编写单元测试来检查 SQL 语法是否正确?我查过一些文章,但大多数都在测试 http 连接状态,但没有测试 SQL。

而我的函数没有返回值,或者我需要重写该函数以具有返回值才能测试它?

下面是我心中的测试结构:

func TestGetURL(t *testing.T) {

    //set const answer for this test

    //set up the mock sql connection

    //call getURL()
    
    //check if equal with answer using assert

}

更新

根据@Emin Laletovic

立即 回答我有一个 testGetURL 的原型。现在我对此有了新的疑问。

func TestGetURL(t *testing.T) {
    //set const answer for this test
    testQuery := `SELECT * FROM "urls" WHERE id=1`
    id := 1

    //set up the mock sql connection
    testDB, mock, err := sqlmock.New()
    if err != nil {
        panic("sqlmock.New() occurs an error")
    }

    // uses "gorm.io/driver/postgres" library
    dialector := postgres.New(postgres.Config{
        DSN:                  "sqlmock_db_0",
        DriverName:           "postgres",
        Conn:                 testDB,
        PreferSimpleProtocol: true,
    })
    db, err = gorm.Open(dialector, &gorm.Config{})
    if err != nil {
        panic("Cannot open stub database")
    }

    //mock the db.Find function
    rows := sqlmock.NewRows([]string{"id", "url", "expire_at", "short_url"}).
        AddRow(1, "http://somelongurl.com", "some_date", "http://shorturl.com")

    mock.ExpectQuery(regexp.QuoteMeta(testQuery)).
        WillReturnRows(rows).WithArgs(id)

    //create response writer and request for testing
    mockedRequest, _ := http.NewRequest("GET", "/1", nil)
    mockedWriter := httptest.NewRecorder()

    //call getURL()
    getURL(mockedWriter, mockedRequest)

    //check values in mockedWriter using assert

}

在代码中,我模拟请求并使用 http、httptest 库进行响应。 我运行测试,但似乎 main.go 中的 getURL 函数无法接收我传入的参数,请参见下图。

db.find调用时,mock.ExpectQuery接收它并开始比较它,到目前为止一切顺利。

db.Find(&url, params["id"])
mock.ExpectQuery(regexp.QuoteMeta(testQuery)).WillReturnRows(rows).WithArgs(id)

根据测试日志,它表明当db.Find触发时,它只执行SELECT * FROM "urls",而不是我期望的SELECT * FROM "urls" " 其中“urls”.“id”= $1

但是当我用 postman 在本地测试 db.Find 并记录 SQL 语法时,它可以正确执行。见下图。

总之,我认为问题是我在getURL(mockedWriter,mockedRequest)中放入的responeWriter/request是错误的,它导致getURL(w http.ResponseWriter, r *http.Request) 无法按我们的预期工作。

如果我遗漏了什么,请告诉我〜

任何重写代码的想法或方法都会有所帮助,谢谢!

I'm new in Go and unit test. I build a samll side projecy called "urlshortener" using Go with Gorm, mux and postgresql.

There is a qeustion annoying me after search many articles.

To make the question clean, I delete some irrelevant code like connect db, .env, etc

My code is below(main.go):

package main

type Url struct {
    ID       uint   `gorm:"primaryKey"` // used for shortUrl index
    Url      string `gorm:"unique"`     // prevent duplicate url
    ExpireAt string
    ShortUrl string
}

var db *gorm.DB
var err error


func main() {
    // gain access to database by getting .env
    ...

    // database connection string
    ...

    // make migrations to the dbif they have not already been created
    db.AutoMigrate(&Url{})

    // API routes
    router := mux.NewRouter()

    router.HandleFunc("/{id}", getURL).Methods("GET")

    router.HandleFunc("/api/v1/urls", createURL).Methods("POST")
    router.HandleFunc("/create/urls", createURLs).Methods("POST")

    // Listener
    http.ListenAndServe(":80", router)

    // close connection to db when main func finishes
    defer db.Close()
}

Now I'm building unit test for getURL function, which is a GET method to get data from my postgresql database called urlshortener and the table name is urls.

Here is getURL function code:

func getURL(w http.ResponseWriter, r *http.Request) {
    params := mux.Vars(r)
    var url Url

    err := db.Find(&url, params["id"]).Error
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
    } else {
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(url.Url)
    }
}

This is work fine with my database. See curl command below:
enter image description here

I know that the unit test is not for mock data, and it aim to test a function/method is stable or not. Although I import mux and net/http for conncetion, but I think the unit test on it should be "SQL syntax". So I decide to focus on testing if gorm return the right value to the test function.

In this case, db.Find will return a *gorm.DB struct which should be exactly same with second line. (see docs https://gorm.io/docs/query.html)

db.Find(&url, params["id"])
SELECT * FROM urls WHICH id=<input_number>

My question is how to write a unit test on it for check the SQL syntax is correct or not in this case (gorm+mux)? I've check some articles, but most of them are testing the http connect status but not for SQL.

And my function do not have the return value, or I need to rewrite the function to have a return value before I can test it?

below is the test structure in my mind:

func TestGetURL(t *testing.T) {

    //set const answer for this test

    //set up the mock sql connection

    //call getURL()
    
    //check if equal with answer using assert

}

Update

According to @Emin Laletovic answer

Now I have a prototype of my testGetURL. Now I have new questions on it.

func TestGetURL(t *testing.T) {
    //set const answer for this test
    testQuery := `SELECT * FROM "urls" WHERE id=1`
    id := 1

    //set up the mock sql connection
    testDB, mock, err := sqlmock.New()
    if err != nil {
        panic("sqlmock.New() occurs an error")
    }

    // uses "gorm.io/driver/postgres" library
    dialector := postgres.New(postgres.Config{
        DSN:                  "sqlmock_db_0",
        DriverName:           "postgres",
        Conn:                 testDB,
        PreferSimpleProtocol: true,
    })
    db, err = gorm.Open(dialector, &gorm.Config{})
    if err != nil {
        panic("Cannot open stub database")
    }

    //mock the db.Find function
    rows := sqlmock.NewRows([]string{"id", "url", "expire_at", "short_url"}).
        AddRow(1, "http://somelongurl.com", "some_date", "http://shorturl.com")

    mock.ExpectQuery(regexp.QuoteMeta(testQuery)).
        WillReturnRows(rows).WithArgs(id)

    //create response writer and request for testing
    mockedRequest, _ := http.NewRequest("GET", "/1", nil)
    mockedWriter := httptest.NewRecorder()

    //call getURL()
    getURL(mockedWriter, mockedRequest)

    //check values in mockedWriter using assert

}

In the code, I mock the request and respone with http, httptest libs.
I run the test, but it seems that the getURL function in main.go cannot receive the args I pass in, see the pic below.
enter image description here

when db.find called, mock.ExpectQuery receive it and start to compare it, so far so good.

db.Find(&url, params["id"])
mock.ExpectQuery(regexp.QuoteMeta(testQuery)).WillReturnRows(rows).WithArgs(id)

According to the testing log, it shows that when db.Find triggerd, it only excute SELECT * FROM "urls" but not I expected SELECT * FROM "urls" WHERE "urls"."id" = $1.

But when I test db.Find on local with postman and log the SQL syntax out, it can be excute properly. see pic below.
enter image description here

In summary, I think the problem is the responeWriter/request I put in getURL(mockedWriter, mockedRequest) are wrong, and it leads that getURL(w http.ResponseWriter, r *http.Request) cannot work as we expect.

Please let me know if I missing anything~

Any idea or way to rewrite the code would be help, thank you!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

世界和平 2025-01-24 17:42:02

如果您只想测试 db.Find 返回的 SQL 字符串,则可以使用 DryRun 功能(根据 文档)。

stmt := db.Session(&Session{DryRun: true}).Find(&url, params["id"]).Statement
stmt.SQL.String() //returns SQL query string without the param value
stmt.Vars // contains an array of input params

但是,要为 getURL 函数编写测试,您可以使用 sqlmock 模拟执行 db.Find 调用时返回的结果。

func TestGetURL(t *testing.T) {
    //set const answer for this test
    testQuery := "SELECT * FROM `urls` WHERE `id` = $1"
    id := 1
    
    //create response writer and request for testing

    //set up the mock sql connection
    testDB, mock, err := sqlmock.New()
    //handle error

    // uses "gorm.io/driver/postgres" library
    dialector := postgres.New(postgres.Config{
        DSN:                  "sqlmock_db_0",
        DriverName:           "postgres",
        Conn:                 testDB,
        PreferSimpleProtocol: true,
    })
    db, err = gorm.Open(dialector, &gorm.Config{})
    //handle error

    //mock the db.Find function
    rows := sqlmock.NewRows([]string{"id", "url", "expire_at", "short_url"}).
        AddRow(1, "http://somelongurl.com", "some_date", "http://shorturl.com")
    mock.ExpectQuery(regexp.QuoteMeta(testQuery)).
        WillReturnRows(rows).WithArgs(id)

    //call getURL()
    getUrl(mockedWriter, &mockedRequest)
    
    //check values in mockedWriter using assert

}

If you just want to test the SQL string that db.Find returns, you can use the DryRun feature (per documentation).

stmt := db.Session(&Session{DryRun: true}).Find(&url, params["id"]).Statement
stmt.SQL.String() //returns SQL query string without the param value
stmt.Vars // contains an array of input params

However, to write a test for the getURL function, you could use sqlmock to mock the results that would be returned when executing the db.Find call.

func TestGetURL(t *testing.T) {
    //set const answer for this test
    testQuery := "SELECT * FROM `urls` WHERE `id` = $1"
    id := 1
    
    //create response writer and request for testing

    //set up the mock sql connection
    testDB, mock, err := sqlmock.New()
    //handle error

    // uses "gorm.io/driver/postgres" library
    dialector := postgres.New(postgres.Config{
        DSN:                  "sqlmock_db_0",
        DriverName:           "postgres",
        Conn:                 testDB,
        PreferSimpleProtocol: true,
    })
    db, err = gorm.Open(dialector, &gorm.Config{})
    //handle error

    //mock the db.Find function
    rows := sqlmock.NewRows([]string{"id", "url", "expire_at", "short_url"}).
        AddRow(1, "http://somelongurl.com", "some_date", "http://shorturl.com")
    mock.ExpectQuery(regexp.QuoteMeta(testQuery)).
        WillReturnRows(rows).WithArgs(id)

    //call getURL()
    getUrl(mockedWriter, &mockedRequest)
    
    //check values in mockedWriter using assert

}
谁对谁错谁最难过 2025-01-24 17:42:02

这篇文章Emin Laletovic 对我帮助很大。

我想我得到了这个问题的答案。

让我们回顾一下这个问题。首先,我使用 gorm 作为 postgresql,使用 mux 作为 http 服务,并构建一个 CRUD 服务。

我需要编写一个单元测试来检查我的数据库语法是否正确(我们假设连接状态为OK),因此我们重点讨论如何编写SQL语法的单元测试。

但是main.go中的处理函数没有返回值,所以我们需要使用mock-sql/ExpectQuery(),当db.Find()时会触发该函数getURL()。通过这样做,我们不必返回一个值来检查它是否与我们的目标匹配。

我在更新中遇到的问题已通过这篇文章< /a>,使用 mux 构建单元测试,但该帖子重点关注状态检查和返回值。

我为此测试设置了 const 答案,id 变量是我们期望得到的。注意到$1我不知道如何更改它,并且我尝试了很多次重写但SQL语法仍然是return $1,也许这是某种我不知道的约束。

//set const answer for this test
    testQuery := `SELECT * FROM "urls" WHERE "urls"."id" = $1`
    id := "1"

我通过 doint this 将值传递到 getURL()

//set the value send into the function
    vars := map[string]string{
        "id": "1",
    }
//create response writer and request for testing
    mockedWriter := httptest.NewRecorder()
    mockedRequest := httptest.NewRequest("GET", "/{id}", nil)
    mockedRequest = mux.SetURLVars(mockedRequest, vars)

最后,我们调用 mock.ExpectationsWereMet() 来检查是否出现任何问题。

if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("SQL syntax is not match: %s", err)
    }

下面是我的测试代码:

func TestGetURL(t *testing.T) {
    //set const answer for this test
    testQuery := `SELECT * FROM "urls" WHERE "urls"."id" = $1`
    id := "1"

    //set up the mock sql connection
    testDB, mock, err := sqlmock.New()
    if err != nil {
        panic("sqlmock.New() occurs an error")
    }

    // uses "gorm.io/driver/postgres" library
    dialector := postgres.New(postgres.Config{
        DSN:                  "sqlmock_db_0",
        DriverName:           "postgres",
        Conn:                 testDB,
        PreferSimpleProtocol: true,
    })
    db, err = gorm.Open(dialector, &gorm.Config{})
    if err != nil {
        panic("Cannot open stub database")
    }

    //mock the db.Find function
    rows := sqlmock.NewRows([]string{"id", "url", "expire_at", "short_url"}).
        AddRow(1, "url", "date", "shorurl")

    //try to match the real SQL syntax we get and testQuery
    mock.ExpectQuery(regexp.QuoteMeta(testQuery)).WillReturnRows(rows).WithArgs(id)

    //set the value send into the function
    vars := map[string]string{
        "id": "1",
    }

    //create response writer and request for testing
    mockedWriter := httptest.NewRecorder()
    mockedRequest := httptest.NewRequest("GET", "/{id}", nil)
    mockedRequest = mux.SetURLVars(mockedRequest, vars)

    //call getURL()
    getURL(mockedWriter, mockedRequest)

    //check result in mockedWriter mocksql built function
    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("SQL syntax is not match: %s", err)
    }

}

我使用 args(1, 1) 和 args(1, 2) 运行了两个测试,效果很好。见下图(请忽略中文)

在此处输入图像描述

This Post and Emin Laletovic are really helps me alot.

I think I get the answer to this qeustion.

Let's recap this questioon. First, I'm using gorm for postgresql and mux for http services and build a CRUD service.

I need to write a unit test to check if my database syntax is correct (we assuming that the connection is statusOK), so we focus on how to write a unit test for SQL syntax.

But the handler function in main.go don't have return value, so we need to use mock-sql/ ExpectQuery(), this function will be triggered when the db.Find() inside getURL(). By doing this, we dont have to return a value to check if it match our target or not.

The problem I met in Update is fixed by This Post, building an unit test with mux, but that post is focusing on status check and return value.

I set the const answer for this test, the id variable is what we expect to get. Noticed that $1 I don't know how to change it, and I've try many times to rewrite but SQL syntax is still return $1, maybe it is some kind of constraint I dont know.

//set const answer for this test
    testQuery := `SELECT * FROM "urls" WHERE "urls"."id" = $1`
    id := "1"

I set the value pass into the getURL() by doint this

//set the value send into the function
    vars := map[string]string{
        "id": "1",
    }
//create response writer and request for testing
    mockedWriter := httptest.NewRecorder()
    mockedRequest := httptest.NewRequest("GET", "/{id}", nil)
    mockedRequest = mux.SetURLVars(mockedRequest, vars)

Finally, we call mock.ExpectationsWereMet() to check if anything went wrong.

if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("SQL syntax is not match: %s", err)
    }

Below is my test code:

func TestGetURL(t *testing.T) {
    //set const answer for this test
    testQuery := `SELECT * FROM "urls" WHERE "urls"."id" = $1`
    id := "1"

    //set up the mock sql connection
    testDB, mock, err := sqlmock.New()
    if err != nil {
        panic("sqlmock.New() occurs an error")
    }

    // uses "gorm.io/driver/postgres" library
    dialector := postgres.New(postgres.Config{
        DSN:                  "sqlmock_db_0",
        DriverName:           "postgres",
        Conn:                 testDB,
        PreferSimpleProtocol: true,
    })
    db, err = gorm.Open(dialector, &gorm.Config{})
    if err != nil {
        panic("Cannot open stub database")
    }

    //mock the db.Find function
    rows := sqlmock.NewRows([]string{"id", "url", "expire_at", "short_url"}).
        AddRow(1, "url", "date", "shorurl")

    //try to match the real SQL syntax we get and testQuery
    mock.ExpectQuery(regexp.QuoteMeta(testQuery)).WillReturnRows(rows).WithArgs(id)

    //set the value send into the function
    vars := map[string]string{
        "id": "1",
    }

    //create response writer and request for testing
    mockedWriter := httptest.NewRecorder()
    mockedRequest := httptest.NewRequest("GET", "/{id}", nil)
    mockedRequest = mux.SetURLVars(mockedRequest, vars)

    //call getURL()
    getURL(mockedWriter, mockedRequest)

    //check result in mockedWriter mocksql built function
    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("SQL syntax is not match: %s", err)
    }

}

And I run two tests with args(1, 1) and args(1, 2), and it works fine. see pic below(please ignore the chinese words)

enter image description here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文