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):
- Como alguien sugirió, ejecute un servidor y envíele las solicitudes HTTP.
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 aServeHTTP
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 }
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.