Empezar a escribir tests automatizados en Go es tan fácil que no requiere de librerías externas, como sí ocurre en otros lenguajes (véase PHPUnit o JUnit). En esta ocasión, el core de Go nos proporciona, tanto los paquetes necesarios (testing) como el binario para la ejecución de los mismos (go test).
Veamos un sencillo ejemplo:
package main
import (
"strings"
"testing"
)
func IsSuperAnimal(animal string) bool {
return strings.ToLower(animal) == "gopher"
}
func TestIsSuperAnimal(t *testing.T) {
expected := true
got := IsSuperAnimal("gopher")
if got != expected {
t.Errorf("Expected: %v, got: %v", expected, got)
}
}
Una vez definido nuestro test, ejecutarlo es tan sencillo como:
go test -cover ./...
Cómo podemos ver, a la ejecución de la tool para ejecutar los tests (go test
) le hemos pasado dos parámetros adicionales.
Veamos para qué sirven:
-cover
nos permite generar un reporte con el coverage de nuestras suites de test../...
nos permite ejecutar todos los tests dentro del directorio actual y los subdirectorios del mismo, de forma recursiva.
Table Driven Design - Cubriendo todos los casos
Por sencilla que sea la lógica que queramos testear, en muchas ocasiones nos encontraremos que un solo test no nos cubre todos los posibles casos. En estas situaciones tenemos dos alternativas:
- Escribir un test para cada caso. E.g.:
TestAdd2Plus2
,TestAdd-2Plus-2
, etc. - Escribir un único test que reciba como parámetro todos los posibles casos (véase TableDrivenTests o DataProvider).
Sin embargo, es muy probable que, a medida que nuestra batería de tests vaya creciendo, tengamos que ponerle cariño para que la mantenibilidad de los mismos no caiga por los suelos. Es por este motivo que, la primera opción suele estar desaconsejada, pues tiende a la duplicación de código, con la pérdida de mantenibilidad que eso conlleva.
Veamos pues qué entendemos por Table Driven Tests:
Escribir bien los tests no siempre es trivial, pero en muchas situaciones se puede cubrir mucho terreno con pruebas basadas en tablas (aka Table Driven Tests).
En este enfoque, definimos una tabla como juego de pruebas (input) para nuestros tests. Cada entrada de la tabla es un caso de prueba completo con las entradas y los resultados esperados, y, algunas veces, con información adicional, como un nombre para identificar cada caso.
Dada una tabla de casos de prueba, el test simplemente itera a través de todas las entradas de la tabla y para cada entrada realiza las pruebas necesarias. El código de test se escribe una vez y se amortiza en todas las entradas de la tabla, por lo que tiene sentido escribir una prueba cuidadosa con buenos mensajes de error.
Siguiendo con el ejemplo anterior, veamos como podríamos añadir más casuísticas a nuestro test, a la vez que nos ahorramos la duplicación de código:
package main
import (
"fmt"
"strings"
"testing"
)
func IsSuperAnimal(animal string) bool {
return strings.ToLower(animal) == "gopher"
}
var isSuperAnimalTests = []struct {
animal string
expected bool
}{
{"elephant", false},
{"python", false},
{"gopher", true},
{"whale", false},
{"gem", false},
}
func TestIsSuperAnimal(t *testing.T) {
for _, tt := range isSuperAnimalTests {
t.Run(tt.animal, func(t *testing.T) {
//t.Parallel()
got := IsSuperAnimal(tt.animal)
if got != tt.expected {
t.Errorf("Expected: %v, got: %v", tt.expected, got)
}
})
}
}
Efectivamente, esta aproximación para la definición de tests es la que se conoce como TableDrivenTests, y, como hemos podido ver, nos permite probar diferentes casuísticas sin tener que escribir varios tests con un código casi idéntico. Además, esta aproximación nos introduce el concepto de subtests, lo que tiene varias implicaciones:
- Por un lado, los subtests nos permiten hacer ejecuciones parciales de nuestros tests. En el ejemplo anterior, podríamos querer lanzar solo los tests que correspondan a algún animal en particular:
go test -run TestIsSuperAnimal/gopher # solo ejecuta los tests de gophers
- Por otro lado, podemos hacer que, la ejecución de cada uno de los casos, se realice de forma concurrente. Para ello haríamos uso de la sentencia que anteriormente teníamos comentada.
t.Parallel()
Assertions
Aquellos lectores que vengáis de otros lenguajes de programación, seguramente estéis echando de menos sentencias cómo:
$this->assertEquals($expected, $got); // php
assertEquals(expected, got); // java
En este sentido, los desarrolladores de Go nos dan libertad, pues podemos:
- Crear nuestros propios métodos de aserción. E.g.:
assertTrue
,assertEquals
, etc. - Usar una librería externa.
A modo de recomendación personal, os aconsejamos echar un vistazo al paquete assert
de la librería testify
y a la librería minimalista is del grandísimo Mat Ryer (@matryer).
Veamos un ejemplo con cada una de ellas:
// testify/assert
package main
import (
"github.com/stretchr/testify/assert"
"strings"
"testing"
)
func IsSuperAnimal(animal string) bool {
return strings.ToLower(animal) == "gopher"
}
func TestIsSuperAnimal(t *testing.T) {
assert.True(t, IsSuperAnimal("gopher"))
}
// is
package main
import (
"github.com/matryer/is"
"strings"
"testing"
)
func IsSuperAnimal(animal string) bool {
return strings.ToLower(animal) == "gopher"
}
func TestIsSuperAnimal(t *testing.T) {
is := is.New(t)
is.True(IsSuperAnimal("gopher"))
}
Mocking
A pesar de que los mocks son considerados, en muchos casos, un antipatrón. Este tipo de test doubles sigue siendo un recurso muy habitual entre los desarrolladores. A lo que nos surge la siguiente pregunta: ¿cómo hacemos mocks en Go? Del mismo modo que cuándo hablábamos de aserciones, en esta ocasión tenemos las dos mismas opciones: implementación propia o librería externa.
Las opciones aquí también son multiples. De hecho, la propia librería testify, anteriormente comentada,
también nos ofrece un paquete mock
. Y, de nuevo, Mat Ryer tiene también una genial librería llamada moq.
Llegó la hora de testear
Cómo habéis visto, empezar con los tests automatizados en Go es coser y cantar. Así que, a partir de ahora, ¡ya no tenéis escusa para seguir con vuestra metodología TDD en Go! Y, si este artículo se os ha quedado corto, no dudéis que pronto llegarán más y mejores artículos relacionados con el testing (test doubles en profundidad, tests de código asíncrono, tests de aceptación, etc). Cómo siempre, no dudéis en comentar con nosotros cualquier duda o sugerencia para futuros artículos.