web-dev-qa-db-ja.com

gorilla / 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))
}
_

これにより、パスで指定されたURLパラメータをエコーするポート_8080_でリッスンする基本的なhttpサーバーが作成されます。したがって、_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.DefaultContext_および_mux.ContextKey_が存在しないため、このソリューションは機能しません。

別の提案された解決策は、リクエスト関数が_map[string]string_を3番目のパラメーターとして受け入れるようにコードを変更することです。その他のソリューションには、実際にサーバーを起動してリクエストを作成し、サーバーに直接送信することが含まれます。私の意見では、これは単体テストの目的を無効にし、それらを本質的に機能テストに変えます。

リンクされたスレッドが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_です。これをチェックする小さなテストアプリを作成しました: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_への直接呼び出しが機能しないのか不思議に思います。

問題は、コンテキスト値を設定する値として_0_を使用しても、mux.Vars()が読み取る値と同じではないことです。 mux.Vars()varsKeyではなくcontextKeyタイプであるintを使用しています(すでに見たように)。

確かに、contextKeyは次のように定義されます。

_type contextKey int
_

つまり、基になるオブジェクトとしてintがありますが、goで値を比較するときにtypeが役割を果たすため、int(0) != contextKey(0)となります。

ゴリラマルチプレクサやコンテキストをだまして値を返すようにする方法はわかりません。


そうは言っても、これをテストするいくつかの方法が頭に浮かびます(以下のコードはテストされていないことに注意してください。ここに直接入力したため、いくつかの愚かなエラーが発生する可能性があります)。

  1. 誰かが示唆したように、サーバーを実行し、HTTPリクエストを送信します。
  2. サーバーを実行する代わりに、テストでgorilla muxルーターを使用します。このシナリオでは、ListenAndServeに渡すルーターが1つありますが、同じルーターインスタンスをテストで使用して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パラメーターを3番目の引数として受け入れ、ラッパーが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を優先します。

20
del-boy

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())
}
18
Luiz Fernando

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

私はこれがより良いアプローチだと思います-リスナーを開始/停止するのが非常に簡単なので、あなたが書いたものを本当にテストしています!

2
Sławosz

次のヘルパー関数を使用して、単体テストからハンドラーを呼び出します。

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

問題は、変数を設定できないことです。

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が必要な場所であればどこでも使用できます。

0
AJcodez