43

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

要遵循的原始问题:


这是我正在尝试做的事情:

main.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()函数的单元测试在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)单元测试?我在这里找到了一些讨论,但建议的解决方案不再有效。建议的解决方案是:

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.DefaultContext并且mux.ContextKey不再存在。

另一个建议的解决方案是更改您的代码,以便请求函数也接受 amap[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 here。所以本质上,关键值是0. 我写了一个小测试应用程序来检查这个: main.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不起作用。

4

5 回答 5

74

gorilla/mux提供SetURLVars用于测试目的的功能,您可以使用它来注入您的 mock 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())
}
于 2018-02-22T17:08:27.373 回答
25

麻烦的是,即使您使用0as value 来设置上下文值,mux.Vars()读取的值也不相同。mux.Vars()正在使用varsKey(如您已经看到的)类型contextKey而不是int.

当然,contextKey定义为:

type contextKey int

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

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


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

  1. 正如有人建议的那样,运行一个服务器并向它发送 HTTP 请求。
  2. 无需运行服务器,只需在测试中使用 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
    }
    
  3. 包装您的处理程序,以便它们接受 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。

于 2015-12-24T18:55:29.170 回答
2

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

于 2015-12-23T14:01:56.077 回答
1

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

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())
}
于 2016-03-13T09:38:46.657 回答
0

问题是你不能设置变量。

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

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

于 2016-09-23T04:07:34.497 回答