En un pasado artículo os explicábamos como crear una API Rest en Go, pero no llegamos a explicar como poder testear dicha API y, como sabemos, testear nuestra aplicación es muy muy importante si queremos poder hacer refactors sin miedo, u obviamente evitar bugs.
¿Pero y cómo testeo los handlers? Pues primeramente deberas estar familiarizado con la forma de testing en GO, cosa que ya os explicábamos en el artículo Empezando con los test automatizados en Go, recordad que se le llama test automatizado a todo test que ha sido automatizado y no tenemos que realizar a mano, luego dentro encontramos distintos tipos como son los Unitarios o Units que son los que veremos ahora.
Tests unitarios para nuestros handlers
Para este ejercicio utilizaremos la API que creamos juntos: GopherApi, y nos basaremos en la tag v0.1. Esto es muy importante porque al terminar este ejercicio subiremos una nueva realease, la v0.1.1, con todo lo que hayamos desarrollado.
Lo primero que tendremos que crear es nuestro fichero de test, en la siguiente ruta, pkg/server/api_test.go
, recordar que los ficheros que Go entiende como testeables son aquellos que acaban en _test.go
, de esta forma el compilador los ignorará y sólo los tendrá en cuenta cuando lancemos los comandos de tests.
Como se trata de un test hacia un endpoint de nuestra api, deberemos emular dicho comportamiento, por ello lo primero será crear una request que llame a nuestro endpoint.
func TestFetchGophers(t *testing.T) {
req, err := http.NewRequest("GET", "/gophers", nil)
if err != nil {
t.Fatalf("could not created request: %v", err)
}
}
Recordemos que en Go, por defecto, no tenemos assertions, sino que somos nosotros los que tenemos que decirle al test que esperamos de él, explícitamente, así pues en la suite nos encontramos con los diferentes métodos:
t.Log/t.Logf()
Con este método podemos printar logs en nuestros tests como si usáramos un Println o un Printf.
t.Error/t.Errorf()
Es lo mismo que el log, pero informamos de que se ha producido un error y de que no pasamos los tests. Además continua con la ejecución.
t.Fatal/t.Fatalf()
Sería lo mismo que error, pero además corta la ejecución en ese momento.
Una vez tenemos nuestra request deberemos testear nuestro handler:
func (a *api) fetchGophers(w http.ResponseWriter, r *http.Request) {
gophers, _ := a.repository.FetchGophers()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(gophers)
}
Hagamos nuestro código testeable
Vemos que para llamarlo necesitaremos una estructura de tipo api, y además el método espera un http.ResponseWriter y un http.Request, veamos cómo resolver esto.
Si vemos nuestro código lo primero que veremos es que, fetchGophers es un método no exportado (o privado, como se le llama en otros lenguajes), esto no debería ser problema, pero vemos que se llama desde el struct api, podríamos resolverlo fácilmente de esta manera:
repo := inmem.NewGopherRepository(sample.Gophers)
a := new(api)
a.repository = repo
Pero, creo que estaréis de acuerdo conmigo en que esto huele un poco mal, recordemos que teníamos una interfaz, llamada Server, la cual utilizábamos, para que el método http.ListenAndServe entendiera como levantar nuestro servidor. Así que hagamos un pequeño refactor en el fichero pkg/server/api.go
type Server interface {
Router() http.Handler
FetchGophers(w http.ResponseWriter, r *http.Request)
FetchGopher(w http.ResponseWriter, r *http.Request)
}
...
func (a *api) FetchGophers(w http.ResponseWriter, r *http.Request) {...}
func (a *api) FetchGopher(w http.ResponseWriter, r *http.Request) {...}
Con este simple cambio podremos pasar de la inicialización anterior a la siguiente.
repo := inmem.NewGopherRepository(sample.Gophers)
s := New(repo)
Mucho más simple y ya no nos suena tan raro ¿verdad? Quizás os estaréis preguntando porque hacemos este refactor en un capítulo sobre tests, y es que una parte fundamental que tenéis que tener en cuenta sobre los tests es que el código que escribamos debe de ser testeable, y eso es lo que hemos hecho.
Ahora sí, veamos como queda nuestro código de test:
func TestFetchGophers(t *testing.T) {
req, err := http.NewRequest("GET", "/gophers", nil)
if err != nil {
t.Fatalf("could not created request: %v", err)
}
repo := inmem.NewGopherRepository(sample.Gophers)
s := New(repo)
rec := httptest.NewRecorder()
s.FetchGophers(rec, req)
}
Ha aparecido algo nuevo en escena: el paquete httptest. Recordemos que la firma del método además de un http.Request que era fácil de conseguir nos pedía un http.ResponseWriter, para ello Go nos facilita de este paquete, con el cual podemos crear un recorder que se encargara de la parte de writer
Bien, si ejecutamos el test, veremos que pasa sin problemas, pero… ¿realmente nos aporta algo este test? simplemente sabemos que se llama a nuestro FetchGophers y que nada falla, podría bastarnos pero vamos un paso más allá, veamos como comprobar que la respuesta que nos da el server es la esperada.
Comprobemos que la respuesta es la esperada
Hemos utilizado antes el httptest.NewRecorder() para poder pasar un http.ResponseWriter, en una ejecución normal de la api, nuestro http.ResponseWriter se encargaría de escribir el contenido, así que ¿por qué no iba a hacerlo el del paquete httptest, veamos como queda en código.
...
res := rec.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Errorf("expected %d, got: %d", http.StatusOK, res.StatusCode)
}
b, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("could not read response: %v", err)
}
var got []*gopher.Gopher
err = json.Unmarshal(b, &got)
if err != nil {
t.Fatalf("could not unmarshall response %v", err)
}
expected := len(sample.Gophers)
if len(got) != expected {
t.Errorf("expected %v gophers, got: %v gopher", sample.Gophers, got)
}
...
Vayamos por partes, lo primero que vemos es rec.Result()
este método nos devuelve una respuesta, gracias a esto podemos investigar que ha pasado en nuestra petición, desde comprobar el código de respuesta:
...
if res.StatusCode != http.StatusOK {
t.Errorf("expected %d, got: %d", http.StatusOK, res.StatusCode)
}
...
Hasta ir un paso más allá y comprobar que la respuesta es la que esperamos, como hacemos en los pasos siguiente:
...
b, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("could not read response: %v", err)
}
var got []*gopher.Gopher
err = json.Unmarshal(b, &got)
if err != nil {
t.Fatalf("could not unmarshall response %v", err)
}
expected := len(sample.Gophers)
if len(got) != expected {
t.Errorf("expected %v gophers, got: %v gopher", sample.Gophers, got)
}
...
Aunque parezca mucha cosa, es bastante sencillo de entender, por un lado leemos el body de la respuesta, luego realizamos un json.Unmarshal de la misma, ya que sabemos que siempre nos va a venir un json como respuesta y luego para no complicarnos en exceso, comprobamos que la cantidad de items, es la misma que en nuestro repositorio.
Ahora es tu turno
Para no alargarnos mucho más hemos preferido no entrar en como testear el otro método de nuestra API, FetchGopher, pero tenéis el ejemplo en el código del repositorio, para este además hemos usado la técnica de Table Drive Design, gracias a esta técnica podemos probar todos los casos con un solo test.
Y bueno, como seguimos queriendo que participéis más en la comunidad, os recordamos que aún nos faltan dos endpoints por crear:
/gophers
(POST) -> Crea un nuevo gopher/gophers/{gopher_id}
(PUT) -> Modifica un gopher existente/gophers/{gopher_id}
(DELETE) -> Elimina el gopher pasado por ID
Y éstos, evidentemente, necesitarán de sus respectivos tests, así que no os cortéis, queremos ver vuestras aportaciones, empezad a mandar vuestros PR (Pull Request) a nuestro repositorio: https://github.com/friendsofgo/gopherapi. Es una buena forma de empezar a colaborar en proyectos open source.