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

Posted

技术标签:

【中文标题】对使用 gorilla/mux URL 参数的函数进行单元测试【英文标题】:Unit testing for functions that use gorilla/mux URL parameters 【发布时间】:2016-03-29 20:37:31 【问题描述】:

TLDR:gorilla/mux 过去不提供设置 URL Vars 的可能性。现在确实如此,这就是为什么在很长一段时间内,第二高的答案是正确答案。

要遵循的原始问题:


这就是我想要做的:

ma​​in.go

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() 函数的单元测试在 ma​​in_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: []byte0x61, 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() 是这样定义的here:

// 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 的值定义为iotahere。所以本质上,关键值是0。我编写了一个小型测试应用程序来检查这一点: ma​​in.go

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 不起作用。

【问题讨论】:

【参考方案1】:

gorilla/mux 提供了用于测试目的的SetURLVars 函数,您可以使用它来注入您的模拟vars

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",
    

    // 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())

【讨论】:

这应该是公认的答案。救了我的后腿!谢谢! 是的,这真的很好用,不会因为无用的功能而使包陷入困境 多么棒的答案!【参考方案2】:

问题是,即使您使用0 作为值来设置上下文值,mux.Vars() 读取的值也不相同。 mux.Vars() 正在使用 varsKey(如您所见),它的类型为 contextKey 而不是 int

当然,contextKey 定义为:

type contextKey int

这意味着它有 int 作为底层对象,但是类型在比较 go 中的值时起作用,所以int(0) != contextKey(0)

我看不出你可以如何欺骗 gorilla mux 或 context 来返回你的值。


话虽如此,我想到了几种测试方法(请注意,下面的代码未经测试,我已直接在此处输入,因此可能会出现一些愚蠢的错误):

    正如有人建议的那样,运行一个服务器并向它发送 HTTP 请求。

    无需运行服务器,只需在测试中使用 gorilla mux Router。在这种情况下,您将拥有一个传递给ListenAndServe 的路由器,但您也可以在测试中使用相同的路由器实例并在其上调用ServeHTTP。路由器将负责设置上下文值,它们将在您的处理程序中可用。

    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
    
    

    包装您的处理程序,以便它们接受 URL 参数作为第三个参数,并且包装器应调用 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。

【讨论】:

谢谢。我想我会使用选项 3。但是 gorilla/mux 应该解决这个问题以使单元测试成为可能。 我通过检查 gorilla/mux 在他们自己的包中进行测试的方式来解决这个问题,并将我自己的断言放在他们正在检查 vars 值的处理程序中:github.com/gorilla/mux/blob/master/context_native_test.go#L22 选项 3 给出此错误(type VarsHandler) as type func(http.ResponseWriter, *http.Request) in argument to router.HandleFunc 我正在使用mux.SetURLVars()【参考方案3】:

在 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 中启动/停止侦听器!

【讨论】:

这不是功能测试吗?我正在尝试进行单元测试,基本上只是像调用任何具有您创建的某些值的函数一样调用该函数并检查结果。 是的。但在我看来,要测试一个网络服务器,功能测试就足够了。在 Go 中,我主要针对复杂的逻辑进行单元测试。 好的。感谢您的提示,但这并不能真正回答问题。如果我不使用带有gorilla/mux 的URL 参数,那么单元测试将起作用,并且基本上我可以为每个端点处理程序函数并行执行测试,而无需通过实际的http 服务器。如果我只使用功能/验收测试,我会避免gorilla/mux 的实际问题,我的测试会花费更长的时间。我计划在我的项目中同时使用功能测试和单元测试。 让我明确一点,我可以在 POST 消息中使用 JSON 来实现相同的目的,并且单元测试将起作用。但这会避免问题,而不是解决问题。【参考方案4】:

我使用以下辅助函数从单元测试中调用处理程序:

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-function:

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

【讨论】:

【参考方案5】:

问题是你不能设置变量。

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

【讨论】:

以上是关于对使用 gorilla/mux URL 参数的函数进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Gorilla Mux 进行 URL 匹配?

GolangWeb 入门 08 集成 Gorilla Mux

GolangWeb 入门 08 集成 Gorilla Mux

使用httputil和gorilla / mux反向代理

使用 Gorilla Mux 和 CockroachDB 编写可维护 RESTful API

如何在 Gorilla mux 路由中获取 OR 模式