web-dev-qa-db-fra.com

Tests unitaires des fonctions utilisant les paramètres d'URL gorilla/mux

Voici ce que j'essaie de faire:

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

Cela crée un serveur http de base à l'écoute sur le port 8080 qui reprend le paramètre d'URL indiqué dans le chemin. Donc, pour http://localhost:8080/test/abcd, il écrira une réponse contenant abcd dans le corps de la réponse.

Le test unitaire de la fonction GetRequest() est dans 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())
}

Le résultat du test est:

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

La question est de savoir comment simuler la mux.Vars(r) pour les tests unitaires? J'ai trouvé des discussions ici mais la solution proposée ne fonctionne plus. La solution proposée était:

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
}

Cette solution ne fonctionne pas car context.DefaultContext et mux.ContextKey n'existent plus.

Une autre solution proposée consisterait à modifier votre code afin que les fonctions de requête acceptent également un map[string]string en tant que troisième paramètre. Parmi les autres solutions, citons le démarrage d'un serveur, la création de la demande et son envoi directement au serveur. À mon avis, cela irait à l'encontre de l'objectif des tests unitaires, en les transformant essentiellement en tests fonctionnels.

Compte tenu du fait que le fil lié est à partir de 2013. Existe-t-il d'autres options?

MODIFIER

J'ai donc lu le code source gorilla/mux et, conformément à mux.go, la fonction mux.Vars() est définie ici comme ceci:

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

La valeur de varsKey est définie par iotaici . Donc, essentiellement, la valeur de la clé est 0. J'ai écrit une petite application de test pour vérifier ceci: 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
}

Qui, lorsqu’il est exécuté, affiche: 

Key: mystring Value: abcd
map[]

Ce qui me fait me demander pourquoi le test ne fonctionne pas et pourquoi l'appel direct à mux.Vars ne fonctionne pas.

Le problème est que, même si vous utilisez 0 comme valeur pour définir des valeurs de contexte, ce n'est pas la même valeur que celle que mux.Vars() lit. mux.Vars() utilise varsKey (comme vous l'avez déjà vu) qui est de type contextKey et non int

Bien sûr, contextKey est défini comme:

type contextKey int

ce qui signifie qu'il a int en tant qu'objet sous-jacent, mais que le type joue un rôle dans la comparaison de valeurs entre go, donc int(0) != contextKey(0)

Je ne vois pas comment vous pourriez tromper gorilla mux ou le contexte pour qu'il retourne vos valeurs. 


Cela étant dit, quelques façons de tester cela me viennent à l'esprit (notez que le code ci-dessous n'a pas été testé, je l'ai saisi directement ici, il peut donc y avoir des erreurs stupides):

  1. Comme quelqu'un l’a suggéré, lancez un serveur et envoyez-lui des requêtes HTTP.
  2. Au lieu d’exécuter le serveur, utilisez simplement gorilla mux Router dans vos tests. Dans ce scénario, vous passeriez un routeur à ListenAndServe, mais vous pourriez également utiliser cette même instance de routeur dans les tests et appeler ServeHTTP dessus. Le routeur se chargerait de la définition des valeurs de contexte et celles-ci seraient disponibles dans vos gestionnaires.

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

    quelque part dans la fonction principale, vous feriez quelque chose comme ceci:

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

    et dans vos tests, vous pouvez faire:

    func TestGetRequest(t *testing.T) {
        r := http.NewRequest("GET", "employees/1", nil)
        w := httptest.NewRecorder()
    
        Router().ServeHTTP(w, r)
        // assertions
    }
    
  3. Enveloppez vos gestionnaires de manière à ce qu’ils acceptent les paramètres d’URL comme troisième argument et l’encapsuleur devrait appeler mux.Vars() et transmettre les paramètres d’URL au gestionnaire.

    Avec cette solution, vos gestionnaires auraient une signature:

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

    et vous auriez à adapter les appels pour se conformer à l'interface http.Handler:

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

    Pour enregistrer un gestionnaire, vous utiliseriez:

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

Celui que vous utilisez est une question de préférence personnelle. Personnellement, je choisirais probablement l’option 2 ou 3, avec une légère préférence pour 3.

18
del-boy

vous devez changer votre test en:

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

J'utilise la fonction d'assistance suivante pour appeler des gestionnaires à partir de tests unitaires:

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

Parce qu'il n'y a pas de moyen (facile) de désenregistrer des gestionnaires HTTP et que plusieurs appels à http.Handle pour le même itinéraire échoueront. Par conséquent, la fonction ajoute une nouvelle route (par exemple, /1 ou /2) pour garantir que le chemin est unique. Cette magie est nécessaire pour utiliser la fonction dans plusieurs unités de test dans le même processus.

Pour tester votre fonction 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

En golang, mon approche des tests est légèrement différente. 

Je réécris légèrement votre code 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))
}

Et voici le test pour cela:

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

Je pense que c'est une meilleure approche - vous testez vraiment ce que vous avez écrit car il est très facile de démarrer/arrêter les auditeurs à la volée!

1
Sławosz

Le problème est que vous ne pouvez pas définir de vars.

var r *http.Request
var key, value string

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

La solution consiste à créer un nouveau routeur à chaque test.

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

Dans votre fichier de gestionnaire.

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

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

Vous pouvez utiliser *api.Route partout où un http.Handler est requis. 

0
AJcodez