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 iota
ici . 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):
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
}
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.
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())
}
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())
}
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!
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.