对使用gorilla/mux URL参数的函数进行单元测试

yuvru6vn  于 2022-12-07  发布在  Go
关注(0)|答案(5)|浏览(180)

TLDR:gorilla/mux过去不提供设置URL变量的功能,现在有了,这就是为什么第二多的支持率的答案在很长一段时间内都是正确的答案。
最初的问题如下:
这就是我要做的:
"主星,开始"

package main

import (
    "fmt"
    "net/http"
    
    "github.com/gorilla/mux"
)
    
func main() {
    mainRouter := mux.NewRouter().StrictSlash(true)
    mainRouter.HandleFunc("/test/{mystring}", GetRequest).Name("/test/{mystring}").Methods("GET")
    http.Handle("/", mainRouter)
    
    err := http.ListenAndServe(":8080", mainRouter)
    if err != nil {
        fmt.Println("Something is wrong : " + err.Error())
    }
}

func GetRequest(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    myString := vars["mystring"]
    
    w.WriteHeader(http.StatusOK)
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte(myString))
}

这会创建一个基本的http服务器监听端口8080,该端口回显路径中给定的URL参数。因此,对于http://localhost:8080/test/abcd,它将回写一个在响应正文中包含abcd的响应。
GetRequest()函数的单元测试位于main_test.go中:

package main

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

    "github.com/gorilla/context"
    "github.com/stretchr/testify/assert"
)

func TestGetRequest(t *testing.T) {
    t.Parallel()
    
    r, _ := http.NewRequest("GET", "/test/abcd", nil)
    w := httptest.NewRecorder()

    //Hack to try to fake gorilla/mux vars
    vars := map[string]string{
        "mystring": "abcd",
    }
    context.Set(r, 0, vars)
    
    GetRequest(w, r)

    assert.Equal(t, http.StatusOK, w.Code)
    assert.Equal(t, []byte("abcd"), w.Body.Bytes())
}

试验结果为:

--- FAIL: TestGetRequest (0.00s)
    assertions.go:203: 
                        
    Error Trace:    main_test.go:27
        
    Error:      Not equal: []byte{0x61, 0x62, 0x63, 0x64} (expected)
                    != []byte(nil) (actual)
            
            Diff:
            --- Expected
            +++ Actual
            @@ -1,4 +1,2 @@
            -([]uint8) (len=4 cap=8) {
            - 00000000  61 62 63 64                                       |abcd|
            -}
            +([]uint8) <nil>
             
        
FAIL
FAIL    command-line-arguments  0.045s

问题是我如何在单元测试中伪造mux.Vars(r)?我找到了一些关于here的讨论,但是提议的解决方案不再起作用。提议的解决方案是:

func buildRequest(method string, url string, doctype uint32, docid uint32) *http.Request {
    req, _ := http.NewRequest(method, url, nil)
    req.ParseForm()
    var vars = map[string]string{
        "doctype": strconv.FormatUint(uint64(doctype), 10),
        "docid":   strconv.FormatUint(uint64(docid), 10),
    }
    context.DefaultContext.Set(req, mux.ContextKey(0), vars) // mux.ContextKey exported
    return req
}

由于context.DefaultContextmux.ContextKey不再存在,因此此解决方案不起作用。
另一个建议的解决方案是修改代码,使请求函数也接受map[string]string作为第三个参数。其他的解决方案包括实际启动一个服务器,构建请求并将其直接发送到服务器。在我看来,这将破坏单元测试的目的,将其本质上变成功能测试。
考虑到链接的线程是2013年的,还有其他选择吗?

编辑

我看过gorilla/mux的源代码,根据mux.go,函数mux.Vars()的定义如下:

// Vars returns the route variables for the current request, if any.
func Vars(r *http.Request) map[string]string {
    if rv := context.Get(r, varsKey); rv != nil {
        return rv.(map[string]string)
    }
    return nil
}

varsKey的值在这里定义为iota。所以本质上,键值是0。我写了一个小的测试应用程序来检查这一点:"主星,开始"

package main

import (
    "fmt"
    "net/http"
    
    "github.com/gorilla/mux"
    "github.com/gorilla/context"
)
    
func main() {
    r, _ := http.NewRequest("GET", "/test/abcd", nil)
    vars := map[string]string{
        "mystring": "abcd",
    }
    context.Set(r, 0, vars)
    what := Vars(r)
        
    for key, value := range what {
        fmt.Println("Key:", key, "Value:", value)
    }

    what2 := mux.Vars(r)
    fmt.Println(what2)
    
    for key, value := range what2 {
        fmt.Println("Key:", key, "Value:", value)
    }

}

func Vars(r *http.Request) map[string]string {
    if rv := context.Get(r, 0); rv != nil {
        return rv.(map[string]string)
    }
    return nil
}

运行时,输出:

Key: mystring Value: abcd
map[]

这让我想知道为什么测试不起作用,为什么直接调用mux.Vars不起作用。

osh3o9ms

osh3o9ms1#

gorilla/mux提供了用于测试的SetURLVars函数,您可以使用该函数注入mock vars

func TestGetRequest(t *testing.T) {
    r, _ := http.NewRequest("GET", "/test/abcd", nil)
    w := httptest.NewRecorder()

    //Hack to try to fake gorilla/mux vars
    vars := map[string]string{
        "mystring": "abcd",
    }

    // CHANGE THIS LINE!!!
    r = mux.SetURLVars(r, vars)
    
    GetRequest(w, r)

    assert.Equal(t, http.StatusOK, w.Code)
    assert.Equal(t, []byte("abcd"), w.Body.Bytes())
}
o2rvlv0m

o2rvlv0m2#

问题是,即使您使用0作为值来设置上下文值,它也不是mux.Vars()读取的值。mux.Vars()使用的是varsKey(正如您已经看到的),它的类型是contextKey,而不是int
确定,contextKey定义为:

type contextKey int

这意味着它的底层对象是int,但是在Go语言中比较值时,type起作用,所以int(0) != contextKey(0)
我看不出你如何能欺骗gorillamux或context返回你的值。
话虽如此,我想到了几种测试方法(注意下面的代码未经测试,我直接在这里输入,所以可能会有一些愚蠢的错误):
1.正如有人建议的那样,运行一个服务器并向其发送HTTP请求。
1.在你的测试中使用gorilla mux Router而不是运行服务器。在这个场景中,你会有一个路由器,你传递给ListenAndServe,但是你也可以在测试中使用同一个路由器示例,并在它上面调用ServeHTTP。Router将负责设置上下文值,它们将在你的处理程序中可用。

func Router() *mux.Router {
    r := mux.Router()
    r.HandleFunc("/employees/{1}", GetRequest)
    (...)
    return r 
}

在main函数中,您可以执行如下操作:

http.Handle("/", Router())

在测试中,您可以:

func TestGetRequest(t *testing.T) {
    r := http.NewRequest("GET", "employees/1", nil)
    w := httptest.NewRecorder()

    Router().ServeHTTP(w, r)
    // assertions
}
  1. Package 处理程序,使其接受URL参数作为第三个参数, Package 程序应调用mux.Vars()并将URL参数传递给处理程序
    使用此解决方案,您的处理程序将具有签名:
type VarsHandler func (w http.ResponseWriter, r *http.Request, vars map[string]string)

并且您必须调整对它的调用以符合http.Handler接口:

func (vh VarsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    vh(w, r, vars)
}

若要注册行程常式,请用途:

func GetRequest(w http.ResponseWriter, r *http.Request, vars map[string]string) {
    // process request using vars
}

mainRouter := mux.NewRouter().StrictSlash(true)
mainRouter.HandleFunc("/test/{mystring}", VarsHandler(GetRequest)).Name("/test/{mystring}").Methods("GET")

你用哪一个是个人喜好的问题。就我个人而言,我可能会选择2或3,略倾向于3。

vzgqcmou

vzgqcmou3#

在golang,我有稍微不同的测试方法。
我稍微重写了您的lib代码:

package main

import (
        "fmt"
        "net/http"

        "github.com/gorilla/mux"
)

func main() {
        startServer()
}

func startServer() {
        mainRouter := mux.NewRouter().StrictSlash(true)
        mainRouter.HandleFunc("/test/{mystring}", GetRequest).Name("/test/{mystring}").Methods("GET")
        http.Handle("/", mainRouter)

        err := http.ListenAndServe(":8080", mainRouter)
        if err != nil {
                fmt.Println("Something is wrong : " + err.Error())
        }
}

func GetRequest(w http.ResponseWriter, r *http.Request) {
        vars := mux.Vars(r)
        myString := vars["mystring"]

        w.WriteHeader(http.StatusOK)
        w.Header().Set("Content-Type", "text/plain")
        w.Write([]byte(myString))
}

下面是对它的测试:

package main

import (
        "io/ioutil"
        "net/http"
        "testing"
        "time"

        "github.com/stretchr/testify/assert"
)

func TestGetRequest(t *testing.T) {
        go startServer()
        client := &http.Client{
                Timeout: 1 * time.Second,
        }

        r, _ := http.NewRequest("GET", "http://localhost:8080/test/abcd", nil)

        resp, err := client.Do(r)
        if err != nil {
                panic(err)
        }
        assert.Equal(t, http.StatusOK, resp.StatusCode)
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
                panic(err)
        }
        assert.Equal(t, []byte("abcd"), body)
}

我认为这是一个更好的方法-你真的是在测试你写的东西,因为它非常容易在go中启动/停止监听器!

pepwfjgg

pepwfjgg4#

我使用下面的helper函数从单元测试中调用处理程序:

func InvokeHandler(handler http.Handler, routePath string,
    w http.ResponseWriter, r *http.Request) {

    // Add a new sub-path for each invocation since
    // we cannot (easily) remove old handler
    invokeCount++
    router := mux.NewRouter()
    http.Handle(fmt.Sprintf("/%d", invokeCount), router)

    router.Path(routePath).Handler(handler)

    // Modify the request to add "/%d" to the request-URL
    r.URL.RawPath = fmt.Sprintf("/%d%s", invokeCount, r.URL.RawPath)
    router.ServeHTTP(w, r)
}

因为没有简单的方法来取消注册HTTP句柄,并且对同一个路由的http.Handle的多次调用将会失败。因此,该函数添加了一个新的路由(例如/1/2)来确保路径是唯一的。在同一个进程中的多个单元测试中使用该函数时,这种魔法是必要的。
要测试GetRequest-函数:

func TestGetRequest(t *testing.T) {
    t.Parallel()

    r, _ := http.NewRequest("GET", "/test/abcd", nil)
    w := httptest.NewRecorder()

    InvokeHandler(http.HandlerFunc(GetRequest), "/test/{mystring}", w, r)

    assert.Equal(t, http.StatusOK, w.Code)
    assert.Equal(t, []byte("abcd"), w.Body.Bytes())
}
zyfwsgd6

zyfwsgd65#

问题是你不能设置变量。

var r *http.Request
var key, value string

// runtime panic, map not initialized
mux.Vars(r)[key] = value

解决方案是在每次测试中创建一个新路由器。

// api/route.go

package api

import (
    "net/http"
    "github.com/gorilla/mux"
)

type Route struct {
    http.Handler
    Method string
    Path string
}

func (route *Route) Test(w http.ResponseWriter, r *http.Request) {
    m := mux.NewRouter()
    m.Handle(route.Path, route).Methods(route.Method)
    m.ServeHTTP(w, r)
}

在你的处理程序文件里。

// api/employees/show.go

package employees

import (
    "github.com/gorilla/mux"
)

func Show(db *sql.DB) *api.Route {
    h := func(w http.ResponseWriter, r http.Request) {
        username := mux.Vars(r)["username"]
        // .. etc ..
    }
    return &api.Route{
        Method: "GET",
        Path: "/employees/{username}",

        // Maybe apply middleware too, who knows.
        Handler: http.HandlerFunc(h),
    }
}

在你的测试中。

// api/employees/show_test.go

package employees

import (
    "testing"
)

func TestShow(t *testing.T) {
    w := httptest.NewRecorder()
    r, err := http.NewRequest("GET", "/employees/ajcodez", nil)
    Show(db).Test(w, r)
}

您可以在需要http.Handler的任何地方使用*api.Route

相关问题