La nueva versión de Go 1.14 esta a la vuelta de la esquina y trae como no iba a ser de otro modo novedades a nuestro lenguaje favorito. Pero hoy nos queremos centrar en el apartado de los tests que como sabéis nos encantan.
Hasta ahora no teníamos una forma nativa de limpiar variables de entorno, dependencias moqueadas, etc de nuestros test de una manera nativa, si bien librerías de terceros nos daban las herramientas, si queríamos limitarnos al paquete de test de la librería estándar, nos encontrábamos con código similar al siguiente.
func TestMain(m *testing.M) {
setup()
code := m.Run()
tearDown()
os.Exit(code)
}
func setup() {
os.Setenv("TEST_VAR", "test")
}
func tearDown() {
os.Setenv("TEST_VAR", "")
}
func Test_helloWorld(t *testing.T) {
got := os.Getenv("TEST_VAR")
expected := "test"
if expected != got {
t.Fatalf("expected: %s, got: %s", expected, got)
}
}
Con este código tenemos a nivel de paquete una forma de poder inicializar y liberar aquellas variables o dependencias que no vayamos a seguir utilizando.
Esto que os puede parecer una trivialidad nos puede llegar a dar grandes dolores de cabeza, imaginemos por un momento que tenemos un ejemplo más normal, que queremos testear un método que depende de nuestra base de datos y tenemos el test siguiente.
var mStore *store.MockStore
func TestMain(m *testing.M) {
setup()
code := m.Run()
tearDown()
os.Exit(code)
}
func setup() {
mStore = &store.MockStore{DB: "some_connection"}
}
func tearDown() {
mStore.Reset()
}
func Test_Add(t *testing.T) {
mStore.Add("some data")
expected := 1
got := mStore.Count()
if expected != got {
t.Fatalf("expected: %d items, got: %d items", expected, got)
}
}
Si ejecutamos nuestro test:
=== RUN Test_Add
--- PASS: Test_Add (0.00s)
PASS
ok test-mod 0.188s
? test-mod/store [no test files]
Todo parece correcto, ¿dónde está la pega? Como dijimos esos métodos nos ayudan a nivel de paquete, pero no sirven igual que un setup
y tearDown
como en otros lenguajes, sino que se ejecutan una sola vez. Por ello si añadimos el siguiente test.
func Test_Substract(t *testing.T) {
mStore.Add("some data")
mStore.Add("some data")
mStore.RemoveLastItem()
expected := 1
got := mStore.Count()
if expected != got {
t.Fatalf("expected: %d items, got: %d items", expected, got)
}
}
Y ejecutamos, obtendremos el siguiente resultado:
=== RUN Test_Add
--- PASS: Test_Add (0.00s)
=== RUN Test_Substract
--- FAIL: Test_Substract (0.00s)
main_test.go:46: expected: 1 items, got: 2 items
FAIL
FAIL test-mod 0.374s
? test-mod/store [no test files]
FAIL
¿Por qué? Pues por lo que hemos dicho, el TestMain
sólo se ejecutará al inicio de los tests de dicho paquete y acabará cuando haya lanzado todos los tests, con lo cual el método Reset
no se llegaría a ejecutar hasta haber lanzado todos nuestros tests.
Podríamos solucionarlo de distintas maneras, podríamos no usar una variable global y realizar la instanciación de nuestros mock en cada test, podríamos lanzar nuestro reset como un defer
en cada test. Pero esto tiene ciertos problemas, tenemos que repetir código en cada test y recordar de realizar el Reset
en cada uno.
Pero es que además si esto fuera un test de integración, y necesitamos realizar un Reset
de datos después de cada test, se podría volver algo complicado, ya que podríamos olvidarnos de colocar algún Reset
y eso repercutir en romper el resto de nuestros tests.
Para ello a partir de la versión 1.14 tendremos a nuestra disposición el nuevo método Cleanup
, veamos como se utiliza.
Cleanup
El método Cleanup
es un método dentro del paquete testing
que pertenece al struct
, testing.T
y que responde a la firma func (t *T) Cleanup(f func())
.
No sólo eso sino que además, es un método “mágico”, que quiero decir con “mágico”, lo que quiero decir es que una vez lo utilicemos el mismo se encargará de ejecutarse al final del test.
Es decir podríamos cambiar nuestro anterior código por algo similar a esto:
var mStore *store.MockStore
func setup(t *testing.T) {
mStore = &store.MockStore{DB: "some_connection"}
t.Cleanup(tearDown)
}
func tearDown() {
mStore.Reset()
}
func Test_Add(t *testing.T) {
setup(t)
mStore.Add("some data")
expected := 1
got := mStore.Count()
if expected != got {
t.Fatalf("expected: %d items, got: %d items", expected, got)
}
}
De esta manera podemos ahorrarnos la función TestMain
y centralizar toda la inicialización y reseteo dentro de nuestra función setup
. Cierto es que en el ejemplo mostrado anteriormente carece un poco de sentido ya que crearíamos una nueva instancia de mstore
cada vez, pero como decía anteriormente, pensad por ejemplo en tests de integración donde tratamos con datos reales de base de datos, sino los borramos tendremos casos nos deseados en nuestros tests.
Conclusión
Gracias a la nueva función Cleanup
podremos ahorrarnos muchas repeticiones de código y error humano a la hora de limpiar nuestros datos, si bien es cierto que ya habían soluciones de terceros con las que podemos emular dicho comportamiento, no viene de más que venga incluido en la librería estándar.
Ahora es tu turno de encontrarle nuevas funcionalidades a este nuevo método y compartirlo con nosotros, ya sea en nuestros comentarios o en nuestro twitter oficial, @FriendsOfGoTech.