it-swarm.com.de

Komponententest für Funktionen, die Gorilla/Mux-URL-Parameter verwenden

Folgendes versuche ich zu tun:

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

Dadurch wird ein grundlegender HTTP-Server erstellt, der auf Port 8080 überwacht wird und der URL-Parameter im Pfad angibt. Also schreibt er für http://localhost:8080/test/abcd eine Antwort zurück, die abcd im Antworttext enthält.

Der Komponententest für die GetRequest()-Funktion befindet sich in 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())
}

Das Testergebnis lautet:

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

Die Frage ist, wie fälsche ich die mux.Vars(r) für die Komponententests? Ich habe einige Diskussionen gefunden hier , aber die vorgeschlagene Lösung funktioniert nicht mehr. Die vorgeschlagene Lösung war:

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
}

Diese Lösung funktioniert nicht, da context.DefaultContext und mux.ContextKey nicht mehr vorhanden sind.

Eine andere vorgeschlagene Lösung wäre, Ihren Code so zu ändern, dass die Anforderungsfunktionen auch einen map[string]string als dritten Parameter akzeptieren. Andere Lösungen umfassen das eigentliche Starten eines Servers, das Erstellen der Anforderung und das direkte Senden an den Server. Meines Erachtens würde dies den Zweck der Komponententests verfehlen und sie im Wesentlichen in Funktionstests umwandeln.

In Anbetracht der Tatsache, dass der verlinkte Thread aus dem Jahr 2013 stammt. Gibt es andere Optionen?

EDIT

Ich habe also den Quellcode gorilla/mux gelesen, und entsprechend mux.go ist die Funktion mux.Vars()hier wie folgt definiert:

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

Der Wert von varsKey ist definiert als iotahier . Im Wesentlichen ist der Schlüsselwert 0. Ich habe eine kleine Test-App geschrieben, um dies zu überprüfen: 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
}

Welche, wenn ausgeführt, Ausgänge: 

Key: mystring Value: abcd
map[]

Deshalb wundere ich mich, warum der Test nicht funktioniert und warum der direkte Aufruf von mux.Vars nicht funktioniert.

Das Problem ist, selbst wenn Sie 0 als Wert zum Festlegen von Kontextwerten verwenden, ist es nicht derselbe Wert, den mux.Vars() liest. mux.Vars() verwendet varsKey (wie Sie bereits gesehen haben) vom Typ contextKey und nicht int

Sicher, contextKey ist definiert als:

type contextKey int

was bedeutet, dass es als zugrunde liegendes Objekt int hat, aber der Typ spielt beim Vergleichen von Werten in go eine Rolle, so int(0) != contextKey(0)

Ich sehe nicht, wie Sie Gorilla-Mux oder Kontext dazu bringen könnten, Ihre Werte zurückzugeben. 


Es gibt jedoch einige Möglichkeiten, dies zu testen. (Beachten Sie, dass der Code unten nicht getestet wurde. Ich habe ihn hier direkt eingegeben, daher könnte es einige dumme Fehler geben):

  1. Führen Sie, wie jemand vorgeschlagen hat, einen Server aus und senden Sie HTTP-Anforderungen an ihn.
  2. Anstatt den Server auszuführen, verwenden Sie einfach einen Gorilla Mux Router in Ihren Tests. In diesem Szenario hätten Sie einen Router, den Sie an ListenAndServe übergeben, Sie könnten jedoch dieselbe Routerinstanz in Tests verwenden und ServeHTTP aufrufen. Der Router kümmert sich um das Festlegen von Kontextwerten, und diese stehen in Ihren Handlern zur Verfügung.

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

    irgendwo in der Hauptfunktion würden Sie so etwas tun:

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

    und in Ihren Tests können Sie Folgendes tun:

    func TestGetRequest(t *testing.T) {
        r := http.NewRequest("GET", "employees/1", nil)
        w := httptest.NewRecorder()
    
        Router().ServeHTTP(w, r)
        // assertions
    }
    
  3. Wickeln Sie Ihre Handler so ein, dass sie URL-Parameter als drittes Argument akzeptieren. Der Wrapper sollte mux.Vars() aufrufen und URL-Parameter an den Handler übergeben.

    Bei dieser Lösung hätten Ihre Handler eine Signatur:

    type VarsHandler func (w http.ResponseWriter, r *http.Request, vars map[string]string)
    

    und Sie müssten Aufrufe daran anpassen, um der http.Handler-Schnittstelle zu entsprechen:

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

    Um den Handler zu registrieren, würden Sie Folgendes verwenden:

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

Welche Sie verwenden, hängt von Ihren persönlichen Vorlieben ab. Ich persönlich würde wahrscheinlich mit Option 2 oder 3 gehen, mit leichter Präferenz in Richtung 3.

18
del-boy

sie müssen Ihren Test ändern in:

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

Ich verwende die folgende Hilfsfunktion, um Handler von Komponententests aufzurufen:

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

Da es keine (einfache) Möglichkeit gibt, HTTP-Handler abzumelden, werden mehrere Aufrufe von http.Handle für dieselbe Route fehlschlagen. Daher fügt die Funktion eine neue Route hinzu (z. B. /1 oder /2), um sicherzustellen, dass der Pfad eindeutig ist. Diese Magie ist notwendig, um die Funktion in mehreren Einheiten im selben Prozess zu verwenden.

So testen Sie Ihre GetRequest- Funktion:

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

In Golang habe ich einen etwas anderen Ansatz zum Testen. 

Ich schreibe Ihren lib-Code leicht um:

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

Und hier ist der Test dafür:

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

Ich denke, das ist ein besserer Ansatz - Sie testen wirklich, was Sie geschrieben haben, da es sehr einfach ist, die Zuhörer zu starten/stoppen!

1
Sławosz

Das Problem ist, dass Sie keine Variablen festlegen können.

var r *http.Request
var key, value string

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

Die Lösung besteht darin, bei jedem Test einen neuen Router zu erstellen.

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

In Ihrer Handlerdatei.

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

In deinen Tests.

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

Sie können *api.Route verwenden, wo immer http.Handler benötigt wird. 

0
AJcodez