2015-12-23 54 views
18

這裏就是我想要做的事:單元測試/ MUX URL參數

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)爲單位測試? 我發現一些討論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年的事實。有沒有其他的選擇?

EDIT

因此,我已閱讀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。我寫了一個小的測試應用程序進行檢查: 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不起作用。

回答

17

問題是,即使使用0作爲設置上下文值的值,它與mux.Vars()讀取的值不同。 mux.Vars()正在使用varsKey(正如您已經看到的),其類型爲contextKey而不是int

當然,contextKey定義爲:

type contextKey int 

,這意味着它具有INT作爲底層對象,但在去比較值時,使int(0) != contextKey(0)型播放部分。

我不明白你怎麼能欺騙大猩猩mux或上下文到返回你的價值觀。


話雖這麼說,兩種方法來測試這個想到(請注意下面的代碼是未經測試,我直接在這裏鍵入它,所以可能有一些愚蠢的錯誤):

  1. 正如有人建議的那樣,運行一個服務器並向它發送HTTP請求。
  2. 而不是運行服務器,只需在測試中使用gorilla mux路由器。在這種情況下,您將有一個路由器傳遞給ListenAndServe,但您也可以在測試中使用相同的路由器實例,並在其上調用ServeHTTP。路由器會負責設置上下文值,它們將在您的處理程序中可用。

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

    某處的主要功能,你會做這樣的事情:

    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

+0

謝謝。我想我會使用選項3.但是'大猩猩/多路複用器'應該解決這個問題,使單元測試成爲可能。 –

+0

我通過檢查gorilla/mux在自己的包中測試的方式解決了這個問題,並且在處理程序中將自己的斷言放在了'vars'的值處:https://github.com/gorilla/mux /blob/master/context_native_test.go#L22 – xentek

1

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

我認爲這是一個更好的方法 - 你真的測試你,因爲它非常寫道容易啓動/停止聽衆進去!

+0

這不是一個功能測試,但?我試圖做一個單元測試,基本上就像調用你創建的某些值的函數一樣調用該函數並檢查結果。 –

+0

就是這樣。但在我看來,要測試網絡服務器,功能測試就足夠了。在進行中,我主要針對複雜的邏輯進行單元測試。 –

+0

好的。謝謝你的提示,但這並沒有真正回答這個問題。如果我不在'gorilla/mux'中使用URL參數,那麼單元測試就可以工作,而且本質上我可以對每個端點處理函數並行執行測試,而不必通過實際的http服務器。如果我只是使用功能/驗收測試,我會避免「大猩猩/多路複用器」的實際問題,我的測試需要更長的時間。我打算在我的項目中使用功能和單元測試。 –

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功能全:

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

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

-2

由於context.setVar不是從Gorilla Mux公開的,他們在2年內還沒有解決這個問題,所以我決定只爲我的服務器做一個解決方法,從頭部取代變量的上下文如果var是空的。由於var不能爲空,所以不會改變我的服務器的功能。

創建一個函數來獲取mux。VARS

func getVar(r *http.Request, key string) string { 
    v := mux.Vars(r)[key] 
    if len(v) > 0 { 
     return v 
    } 
    return r.Header.Get("X-UNIT-TEST-VAR-" + key) 
} 

然後,而不是

vars := mux.Vars(r) 
myString := vars["mystring"] 

只需撥打

myString := getVar("mystring") 

在單元測試中這意味着你可以添加一個功能

func setVar(r *http.Request, key string, value string) { 
    r.Header.Set("X-UNIT-TEST-VAR-"+key, value) 
} 

然後,讓你的要求

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

您需要將您的測試更改爲:

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