Pruebas unitarias para APIs REST basadas en Go Gin Web Framework con httptest
Introducción
Gin Web Framework es uno de los frameworks más populares de Go para construir APIs RESTful. Cuando implementas tus endpoints REST o HTTP, especialmente si sigues un enfoque de desarrollo guiado por pruebas (TDD), lo más probable es que quieras escribir pruebas unitarias para verificar tus endpoints.
El paquete net/http/httptest es una librería estándar que proporciona utilidades para probar servidores y clientes HTTP.
En este artículo, te mostraré cómo puedes probar tus APIs RESTful basadas en Gin-Gonic usando el paquete httptest para probar las peticiones y respuestas HTTP.
El API probado
Para poder mostrar las capacidades de pruebas del paquete httptest, he implementado una aplicación API RESTful sencilla usando Gin.
La aplicación expone un único endpoint en la ruta raíz /
que permite operaciones de creación, lectura, actualización y eliminación (CRUD) para un servicio de almacenamiento de objetos multipropósito ficticio.
Este es un proyecto de demostración sencillo, los objetos manejados por la API se almacenan en memoria y no se persisten.
Los endpoints expuestos por la aplicación son los siguientes:
GET /
:- Devuelve una lista de todos los objetos del servicio para peticiones
Accept: application/json
- Devuelve el nombre de la aplicación en cualquier otro caso
- Devuelve una lista de todos los objetos del servicio para peticiones
POST /
: Crea un nuevo objeto y le asigna un nuevoid
PUT /:id
: Actualiza o inserta el objeto facilitado para elid
proporcionadoDELETE /:id
: Elimina el objeto con elid
proporcionado
El siguiente fragmento de código muestra la función de configuración del router Gin:
func SetupRouter() *gin.Engine {
router := gin.Default()
router.GET("/", addCommonHeaders, get, fallbackGet)
router.POST("/", addCommonHeaders, post)
router.PUT("/:id", addCommonHeaders, put)
router.DELETE("/:id", addCommonHeaders, remove)
return router
}
Puedes encontrar la implementación completa del router en GitHub (router.go
).
La función SetupRouter
se usa posteriormente por la función main
de la aplicación para iniciar el servidor:
err := router.SetupRouter().Run("0.0.0.0:8080")
No obstante, la función SetupRouter
también se usará por las pruebas para registrar las respuestas del router.
Veámoslo a continuación.
Cómo escribir una prueba sencilla
El siguiente fragmento de código muestra el test completo para el endpoint GET /
cuando se trata una petición sin Accept
header.
En este caso, la aplicación debería devolver un código de estado 200
, el nombre de la aplicación en el cuerpo y algunas cabeceras estándar.
func TestFallbackGet(t *testing.T) {
router := SetupRouter()
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, httptest.NewRequest("GET", "/", nil))
t.Run("Returns 200 status code", func(t *testing.T) {
if recorder.Code != 200 {
t.Error("Expected 200, got ", recorder.Code)
}
})
t.Run("Returns app name", func(t *testing.T) {
if recorder.Body.String() != "\"Cocktail service\"" {
t.Error("Expected '\"Cocktail service\"', got ", recorder.Body.String())
}
})
t.Run("Returns Server header", func(t *testing.T) {
if recorder.Header().Get("Server") != "gin-gonic/1.33.7" {
t.Error("Expected 'gin-gonic/1.33.7', got ", recorder.Header().Get("Server"))
}
})
}
Comenzamos la implementación del test creando un nuevo router Gin invocando la función SetupRouter
de la aplicación.
A continuación, creamos un nuevo httptest.Recorder
. Lo emplearemos para capturar la respuesta del router.
Por último, invocamos la función ServeHTTP
del router pasándole el recorder y una nueva httptest.Request
con la ruta GET /
y sin cuerpo.
Esta es la sección de Acción de la prueba, que consta de dos partes:
Generando la petición usando httptest.NewRequest
httptest.NewRequest("GET", "/", nil)
El primer argumento es el método HTTP, el segundo es la ruta y el tercero es el cuerpo de la petición.
Invocando la función ServeHTTP del router
router.ServeHTTP(recorder, request)
El primer argumento es el recorder que creamos previamente y el segundo es la petición.
La implementación del test continúa con múltiples subtests para verificar las diferentes partes de la respuesta. Veamos cómo.
Verificando el código de estado
La primera verificación en el test TestFallbackGet
se asegura de que el código de estado devuelto por el router es 200
.
Para ello, comprobamos el valor de la propiedad recorder.Code
que contiene el código de estado HTTP de la respuesta.
Debido a que no estamos usando ninguna librería o framework de testing, comparamos manualmente el valor con el esperado.
if recorder.Code != 200 {
t.Error("Expected 200, got ", recorder.Code)
}
Si la verificación falla, usamos la función t.Error
para reportar el error.
Verificando el cuerpo de la respuesta
La siguiente comprobación en el test TestFallbackGet
verifica que el cuerpo de la respuesta del servidor contiene el nombre de la aplicación (Cocktail service
).
Como se trata de un literal de texto plano, podemos usar la función recorder.Body.String()
para obtener el cuerpo de la respuesta como una cadena/string.
if recorder.Body.String() != "\"Cocktail service\"" {
t.Error("Expected '\"Cocktail service\"', got ", recorder.Body.String())
}
A continuación, comparamos el valor con el esperado de la misma forma que hicimos con el código de estado y reportamos el error si la comprobación falla.
Verificando las cabeceras de la respuesta
La última verificación en el test TestFallbackGet
comprueba que la respuesta del servidor contiene la cabecera/header Server
con el motor y la versión de la aplicación.
Esta cabecera la establecemos manualmente en la función addCommonHeaders
de la aplicación.
La función addCommonHeaders
es un handler que se añade a todas las rutas de la aplicación para establecer algunas cabeceras de respuesta que son comunes a todos los endpoints:
func addCommonHeaders(c *gin.Context) {
c.Header("Cache-Control", "no-cache, no-store")
c.Header("Server", "gin-gonic/1.33.7")
}
Para comprobar que la cabecera Server
está presente en la respuesta, usamos la función recorder.Header().Get("Server")
para obtener su valor.
if recorder.Header().Get("Server") != "gin-gonic/1.33.7" {
t.Error("Expected 'gin-gonic/1.33.7', got ", recorder.Header().Get("Server"))
}
A continuación, comparamos el valor con el esperado de la misma forma que hicimos con el código de estado y el cuerpo de la respuesta y reportamos el error si la comprobación falla.
Estos escenarios para verificar los componentes básicos de la respuesta son muy sencillos. Sin embargo, podemos usar el mismo enfoque para probar escenarios más complejos. Veamos ahora cómo probar escenarios más complejos para verificar el comportamiento completo de nuestra API RESTful.
Probando escenarios más complejos
El test TestFallbackGet
que hemos visto en la sección anterior es una prueba muy sencilla que verifica los componentes básicos de la respuesta para una petición HTTP GET
.
No obstante, podemos usar el mismo enfoque para probar escenarios más complejos como diferentes métodos HTTP, cabeceras, cuerpos de petición, cuerpos de respuesta, etc.
Creando una petición
La aplicación probada tiene un único endpoint que admite varios métodos HTTP. Uno de los métodos aceptados es POST
.
Al realizar una petición POST
, la aplicación espera un cuerpo JSON que contenga un objeto y también una cabecera con el Content-Type
establecido a application/json
.
Podemos usar el paquete httptest para crear una petición con estos requisitos de forma fácil:
request := httptest.NewRequest("POST", "/", strings.NewReader(`{
"name": "Bloody Mary",
"rating": 1
}`))
request.Header.Add("Content-Type", "application/json")
En la primera línea, creamos una nueva httptest.Request
con un método POST
a la ruta /
y un cuerpo JSON que contiene un objeto.
Para crear el cuerpo, usamos la función strings.NewReader
para crear un nuevo io.Reader
a partir de un literal de cadena/string.
El literal de cadena/string contiene la representación JSON del objeto que queremos enviar al servidor.
La siguiente línea añade la cabecera Content-Type
a la petición usando la función Request.Header.Add
.
Esta petición ahora puede usarse para invocar la función ServeHTTP
del router y registrar la respuesta.
router.ServeHTTP(recorder, request)
Verificando el cuerpo de una respuesta JSON
Cuando realizamos una petición POST
con un cuerpo JSON que contiene un objeto, la aplicación asigna automáticamente un id
al objeto y lo devuelve en el cuerpo de la respuesta.
Este y otros escenarios se prueban en el test TestPostValid
.
El siguiente fragmento de código muestra el subtest que verifica que el objeto devuelto contiene el id
generado:
t.Run("Returns saved object with id", testCase(func(t *testing.T, c *context) {
c.router.ServeHTTP(c.recorder, reqBuilder())
var body map[string]interface{}
json.Unmarshal(c.recorder.Body.Bytes(), &body)
matched, _ := regexp.MatchString(
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
body["id"].(string),
)
if !matched {
t.Error("Expected object with id, got ", body)
}
}))
En este caso, usamos la función json.Unmarshal
para procesar el cuerpo de la respuesta y guardarlo en un map[string]interface{}
.
Luego, usamos una expresión regular para verificar que la propiedad id
del objeto coincide con el formato de un UUID.
Para ello, usamos la función regexp.MatchString
para realizar la comprobación de la expresión regular.
El fichero router_test.go
contiene una suite de pruebas completa que verifica todo el comportamiento de la aplicación (además de ofrecer un 100% de cobertura de código).
Por favor, asegúrate de revisarla.
Conclusión
En este artículo, hemos visto cómo probar una API RESTful basada en Gin Web Framework usando el paquete net/http/httptest
de Go.
Hemos comenzado definiendo el comportamiento de la aplicación.
Luego, hemos implementado algunas pruebas para verificar los diferentes componentes de la respuesta a una petición HTTP GET
sencilla.
Por último, hemos visto cómo probar escenarios más complejos como diferentes métodos HTTP, cabeceras, cuerpos de petición, cuerpos de respuesta, etc.
Siguiendo estas prácticas de pruebas, puedes asegurar la robustez y fiabilidad de tus APIs.
Puedes encontrar el código fuente del artículo en GitHub.
¡Feliz testing!