visual unitarias tipos studio pruebas plan interfaz ejemplos ejemplo crear unit-testing go gorilla testify

unit testing - tipos - Pruebas unitarias para funciones que usan parámetros de URL de gorila/mux



pruebas unitarias interfaz (5)

Esto es lo que trato de hacer:

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

Esto crea un servidor HTTP básico que escucha en el puerto 8080 que hace eco del parámetro de URL dado en la ruta. Entonces para http://localhost:8080/test/abcd escribirá una respuesta que contenga abcd en el cuerpo de la respuesta.

La prueba unitaria para la función GetRequest() está en 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()) }

El resultado de la prueba es:

--- 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 pregunta es ¿cómo falsifico el mux.Vars(r) para las pruebas unitarias? He encontrado algunas discusiones aquí, pero la solución propuesta ya no funciona. La solución propuesta fue:

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 }

Esta solución no funciona porque context.DefaultContext y mux.ContextKey ya no existen.

Otra solución propuesta sería modificar su código para que las funciones de solicitud también acepten una map[string]string como un tercer parámetro. Otras soluciones incluyen realmente iniciar un servidor y generar la solicitud y enviarla directamente al servidor. En mi opinión, esto vencería el objetivo de las pruebas unitarias, convirtiéndolas esencialmente en pruebas funcionales.

Teniendo en cuenta el hecho de que el hilo vinculado es de 2013. ¿Hay alguna otra opción?

EDITAR

Así que he leído el código fuente de gorilla/mux , y de acuerdo con mux.go la función mux.Vars() se define aquí así:

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

El valor de varsKey se define como iota aquí . Entonces, esencialmente, el valor clave es 0 . He escrito una pequeña aplicación de prueba para verificar esto: 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 }

Que cuando se ejecuta, produce:

Key: mystring Value: abcd map[]

Lo que me hace preguntarme por qué la prueba no funciona y por qué la llamada directa a mux.Vars no funciona.


En Golang, tengo un enfoque ligeramente diferente para las pruebas.

Reescribo ligeramente tu código de 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)) }

Y aquí está la prueba:

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

Creo que este es un mejor enfoque: realmente estás probando lo que escribiste ya que es muy fácil comenzar / detener a los oyentes en el camino.


Uso la siguiente función auxiliar para invocar manejadores de pruebas unitarias:

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

Porque no existe una forma (fácil) de cancelar el registro de manejadores HTTP y múltiples llamadas a http.Handle para la misma ruta fallará. Por lo tanto, la función agrega una nueva ruta (por ejemplo, /1 o /2 ) para garantizar que la ruta sea única. Esta magia es necesaria para usar la función en la prueba de unidades múltiples en el mismo proceso.

Para probar su función 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()) }


El problema es que no puedes establecer vars.

var r *http.Request var key, value string // runtime panic, map not initialized mux.Vars(r)[key] = value

La solución es crear un nuevo enrutador en cada prueba.

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

En tu archivo de controlador.

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

En tus pruebas

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

Puede usar *api.Route siempre que se necesite un http.Handler .


Como context.setVar no es público desde Gorilla Mux, y no han solucionado este problema en más de 2 años, decidí que simplemente haría una solución para mi servidor que obtenga la variable de un encabezado en lugar del contexto si el var está vacío. Como la var nunca debe estar vacía, esto no cambia la funcionalidad de mi servidor.

Crea una función para obtener 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) }

Entonces, en lugar de

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

Solo llama

myString := getVar("mystring")

Lo que significa que en las pruebas de tu unidad puedes agregar una función

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

Luego haga su solicitud

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


El problema es que, incluso cuando usa 0 como valor para establecer valores de contexto, no es el mismo valor que mux.Vars() lee. mux.Vars() está utilizando varsKey (como ya has visto) que es de tipo contextKey y no int .

Claro, contextKey se define como:

type contextKey int

lo que significa que tiene int como objeto subyacente, pero el tipo juega una parte cuando se comparan los valores en go, por lo que int(0) != contextKey(0) .

No veo cómo puedes engañar a gorila mux o al contexto para que devuelvan tus valores.

Dicho esto, me vienen a la mente algunas maneras de probar esto (tenga en cuenta que el siguiente código no se ha probado, lo he escrito directamente aquí, por lo que podría haber algunos errores estúpidos):

  1. Como alguien sugirió, ejecute un servidor y envíele las solicitudes HTTP.
  2. En lugar de ejecutar el servidor, solo use el enrutador gorilla mux en sus pruebas. En este escenario, tendría un enrutador que pasaría a ListenAndServe , pero también podría usar esa misma instancia de enrutador en las pruebas y llamar a ServeHTTP en él. El enrutador se ocuparía de establecer los valores de contexto y estarían disponibles en sus controladores.

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

    en algún lugar de la función principal, harías algo como esto:

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

    y en tus pruebas puedes hacer:

    func TestGetRequest(t *testing.T) { r := http.NewRequest("GET", "employees/1", nil) w := httptest.NewRecorder() Router().ServeHTTP(w, r) // assertions }

  3. Envuelva sus controladores para que acepten los parámetros de URL como tercer argumento y wrapper debe llamar a mux.Vars() y pasar los parámetros de URL al controlador.

    Con esta solución, sus controladores tendrían firma:

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

    y tendría que adaptar las llamadas para cumplir con la interfaz http.Handler :

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

    Para registrar el controlador, debe usar:

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

Cuál de ellos usa es cuestión de preferencia personal. Personalmente, probablemente iría con la opción 2 o 3, con ligera preferencia hacia 3.