Cómo preparar y desmontar tests unitarios en Go
Introducción
Cuando escribimos pruebas unitarias en Go, a veces es útil realizar acciones comunes que deben ejecutarse antes y después de un test. Estas tareas de preparación y desmontaje pueden ser necesarias en diferentes ámbitos:
- Antes y después de que se ejecute la suite completa de pruebas.
- Antes y después de que se ejecute cada prueba.
- O incluso antes y después de que se ejecute un grupo concreto de pruebas.
Procedente de un entorno Java, esta es una práctica común. Los frameworks de pruebas como JUnit tienen anotaciones integradas que proporcionan esta funcionalidad de forma aspect-oriented. Sin embargo, lograr lo mismo en Go con la biblioteca estándar de pruebas ha sido un poco esquivo. En este artículo, te mostraré cómo conseguirlo en Go.
Ejecutar código antes y después de la suite completa de tests
A partir de Go 1.4, la biblioteca estándar proporciona una forma de ejecutar código antes y después de que se ejecute la suite completa de pruebas. Si un fichero de test en un paquete contiene una función con la firma:
func TestMain(m *testing.M)
Go ejecutará esa función en lugar de ejecutar las pruebas directamente. Puedes aprender más sobre esta función en la documentación de la biblioteca estándar de Go.
Una implementación típica de esta función sería:
func TestMain(m *testing.M) {
// Setup code goes here
code := m.Run()
// Teardown code goes here
os.Exit(code)
}
Esta función nos permite ejecutar código antes y después de que se ejecute la suite completa de pruebas.
Para ejecutar las pruebas, necesitamos llamar a m.Run()
.
Nótese cómo almacenamos el resultado de m.Run()
en una variable y luego llamamos a os.Exit(code)
para salir del programa con el código de salida adecuado.
Cuestiones a tener en cuenta:
- Puedes definir esta función en cualquiera de los ficheros de test de tu paquete.
- Los nombres de las funciones son únicos en un paquete, por lo que sólo puedes definir una función
TestMain
por paquete. - Si tu paquete contiene varios ficheros de test, coloca la función
TestMain
donde tenga más sentido. - Si no llamas a
os.Exit
con el código de retorno dem.Run()
, tu comando de test siempre devolverá0
(incluso si falla).
Veamos ahora cómo ejecutar código antes y después de un grupo de tests.
Ejecutar código antes y después de un grupo de tests
Otro escenario común es ejecutar código o preparar el entorno de pruebas antes de que se ejecute un grupo de tests. Los subtests de Go son muy útiles en este caso.
Los Subtests emplean la función t.Run(name string, f func(t *T))
para ejecutar la función f
como un subtest de t
.
Veamos un ejemplo:
func TestSomethingWhenGivenCondition(t *testing.T) {
// Setup code for given condition goes here
t.Run("Something has property one", func(t *testing.T) {
// Subtest 1 code goes here
})
t.Run("Something has property two", func(t *testing.T) {
// Subtest 2 code goes here
})
// Teardown code for given condition goes here
}
Supongamos que queremos probar el comportamiento de nuestra aplicación en un entorno con condiciones específicas.
Los subtests nos permiten agrupar estas pruebas bajo una función, en este caso, TestSomethingWhenGivenCondition
.
Esta función nos permite configurar y desmontar el entorno ejecutando código antes y después de la invocación de cada subtest.
A continuación, podemos ejecutar un subtest usando la función t.Run
para cada propiedad o comportamiento de la aplicación que queremos probar.
Puedes encontrar un ejemplo completo en este enlace. Veamos ahora cómo ejecutar código antes y después de cada test individual.
Ejecutar código antes y después de cada test
La biblioteca estándar de pruebas de Go no proporciona una forma integrada de ejecutar código antes y después de cada test. Si queremos hacer esto, necesitamos proporcionar una implementación alternativa. Esto es lo que suelo hacer.
Define un struct
para contener el contexto del test que se inicializará y desmontará antes y después de cada test.
type testContext struct {
some string
test *pkg.Var
context int64
}
func (c *testContext) beforeEach() {
// Setup code for context goes here
}
func (c *testContext) afterEach() {
// Teardown code for context goes here
}
A continuación, define una función que se utilizará para encapsular cada test:
func testCase(test func(t *testing.T, c *testContext)) func(*testing.T) {
return func(t *testing.T) {
context := &testContext{}
context.beforeEach()
defer context.afterEach()
test(t, context)
}
}
Por último, utiliza esta función en un subtest:
func TestSomething(t *testing.T) {
t.Run("Some test case", testCase(func(t *testing.T, c *testContext) {
// Test code goes here which can leverage the context
}))
}
Por cada invocación de test, se crea un nuevo testContext
y se llaman a las funciones beforeEach
y afterEach
antes y después de que se ejecute el test.
Puedes encontrar un ejemplo completo en este enlace.
Conclusión
En este artículo, hemos visto cómo configurar y desmontar correctamente un test unitario en Go usando la biblioteca estándar de Go para pruebas unitarias. Hemos visto cómo ejecutar código antes y después de la suite completa de pruebas, antes y después de un grupo de pruebas, y antes y después de cada prueba. Estas técnicas te ayudarán a escribir pruebas robustas y bien organizadas en tus proyectos Go.
¡Feliz testing!