1. Introducción a las pruebas unitarias
Las pruebas unitarias se refieren a verificar y validar la unidad más pequeña que se puede probar en un programa, como una función o un método en el lenguaje Go. Las pruebas unitarias aseguran que el código funcione como se espera y permiten a los desarrolladores realizar cambios en el código sin romper accidentalmente la funcionalidad existente.
En un proyecto de Golang, la importancia de las pruebas unitarias es innegable. En primer lugar, puede mejorar la calidad del código, brindando a los desarrolladores más confianza para realizar cambios en el código. En segundo lugar, las pruebas unitarias pueden servir como documentación para el código, explicando su comportamiento esperado. Además, ejecutar pruebas unitarias automáticamente en un entorno de integración continua puede descubrir rápidamente errores recién introducidos, mejorando así la estabilidad del software.
2. Realización de pruebas básicas usando el paquete testing
La biblioteca estándar del lenguaje Go incluye el paquete testing
, que proporciona herramientas y funcionalidades para escribir y ejecutar pruebas.
2.1 Crear tu primer caso de prueba
Para escribir una función de prueba, debes crear un archivo con el sufijo _test.go
. Por ejemplo, si tu archivo de código fuente se llama calculator.go
, tu archivo de prueba debería llamarse calculator_test.go
.
A continuación, es el momento de crear la función de prueba. Una función de prueba necesita importar el paquete testing
y seguir un formato específico. Aquí tienes un ejemplo sencillo:
// calculator_test.go
package calculator
import (
"testing"
"fmt"
)
// Probar la función de suma
func TestAdd(t *testing.T) {
resultado := Add(1, 2)
esperado := 3
if resultado != esperado {
t.Errorf("Esperado %v, pero se obtuvo %v", esperado, resultado)
}
}
En este ejemplo, TestAdd
es una función de prueba que prueba una función imaginaria Add
. Si el resultado de la función Add
coincide con el resultado esperado, la prueba pasará; de lo contrario, se llamará a t.Errorf
para registrar la información sobre el fallo de la prueba.
2.2 Entender las reglas de nomenclatura y la firma de las funciones de prueba
Las funciones de prueba deben comenzar con Test
, seguido de cualquier cadena que no sea minúscula, y su único parámetro debe ser un puntero a testing.T
. Como se muestra en el ejemplo, TestAdd
sigue las reglas de nomenclatura y firma correctas.
2.3 Ejecutar casos de prueba
Puedes ejecutar tus casos de prueba utilizando la herramienta de línea de comandos. Para un caso de prueba específico, ejecuta el siguiente comando:
go test -v // Ejecuta pruebas en el directorio actual y muestra una salida detallada
Si deseas ejecutar un caso de prueba específico, puedes usar el indicador -run
seguido de una expresión regular:
go test -v -run TestAdd // Ejecutar solo la función de prueba TestAdd
El comando go test
encontrará automáticamente todos los archivos _test.go
y ejecutará cada función de prueba que cumpla con los criterios. Si todas las pruebas pasan, verás un mensaje similar a PASS
en la línea de comandos; si alguna prueba falla, verás FAIL
junto con el mensaje de error correspondiente.
3. Escribir casos de prueba
3.1 Reportar errores utilizando t.Errorf
y t.Fatalf
En el lenguaje Go, el marco de pruebas proporciona varios métodos para reportar errores. Las dos funciones más comúnmente utilizadas son Errorf
y Fatalf
, ambas son métodos del objeto testing.T
. Errorf
se utiliza para reportar errores en la prueba pero no detiene el caso de prueba actual, mientras que Fatalf
detiene la prueba actual inmediatamente después de reportar un error. Es importante elegir el método apropiado en función de los requisitos de la prueba.
Ejemplo de uso de Errorf
:
func TestAdd(t *testing.T) {
obtenido := Add(1, 2)
esperado := 3
if obtenido != esperado {
t.Errorf("Add(1, 2) = %d; esperado %d", obtenido, esperado)
}
}
Si deseas detener la prueba inmediatamente cuando se detecta un error, puedes usar Fatalf
:
func TestSubtract(t *testing.T) {
obtenido := Subtract(5, 3)
if obtenido != 2 {
t.Fatalf("Subtract(5, 3) = %d; esperado 2", obtenido)
}
}
En general, si el error causaría que el código posterior no se ejecute correctamente o el fallo de la prueba se puede confirmar por adelantado, se recomienda utilizar Fatalf
. De lo contrario, se recomienda utilizar Errorf
para obtener un resultado de prueba más completo.
3.2 Organización de subpruebas y ejecución de subpruebas
En Go, podemos utilizar t.Run
para organizar subpruebas, lo que nos ayuda a escribir código de prueba de una manera más estructurada. Las subpruebas pueden tener su propio Setup
y Teardown
y pueden ejecutarse individualmente, lo que proporciona una gran flexibilidad. Esto es especialmente útil para realizar pruebas complejas o pruebas parametrizadas.
Ejemplo de uso de subprueba t.Run
:
func TestMultiply(t *testing.T) {
testcases := []struct {
name string
a, b, expected int
}{
{"2x3", 2, 3, 6},
{"-1x-1", -1, -1, 1},
{"0x4", 0, 4, 0},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if got := Multiply(tc.a, tc.b); got != tc.expected {
t.Errorf("Multiply(%d, %d) = %d; quiero %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
Si queremos ejecutar la subprueba llamada "2x3" individualmente, podemos ejecutar el siguiente comando en la línea de comandos:
go test -run TestMultiply/2x3
Por favor, ten en cuenta que los nombres de las subpruebas distinguen entre mayúsculas y minúsculas.
4. Trabajo preparatorio antes y después de las pruebas
4.1 Configuración y limpieza
Al llevar a cabo pruebas, a menudo necesitamos preparar algún estado inicial para las pruebas (como conexión a base de datos, creación de archivos, etc.), y de manera similar, necesitamos realizar un trabajo de limpieza después de que las pruebas se completen. En Go, generalmente realizamos la Configuración
y Limpieza
directamente en las funciones de prueba, y la función t.Cleanup
nos proporciona la capacidad de registrar funciones de devolución de llamada de limpieza.
Aquí tienes un ejemplo simple:
func TestDatabase(t *testing.T) {
db, err := SetupDatabase()
if err != nil {
t.Fatalf("error en la configuración: %v", err)
}
// Registra una devolución de llamada de limpieza para garantizar que la conexión a la base de datos se cierre cuando la prueba esté terminada
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("error al cerrar la base de datos: %v", err)
}
})
// Realiza la prueba...
}
En la función TestDatabase
, primero llamamos a la función SetupDatabase
para configurar el entorno de prueba. Luego, utilizamos t.Cleanup()
para registrar una función que se llamará después de que la prueba se complete para realizar el trabajo de limpieza, en este ejemplo, es cerrar la conexión a la base de datos. De esta manera, podemos garantizar que los recursos se liberen correctamente independientemente de si la prueba tiene éxito o falla.
5. Mejorar la eficiencia de las pruebas
Mejorar la eficiencia de las pruebas puede ayudarnos a iterar el desarrollo más rápidamente, descubrir rápidamente problemas y garantizar la calidad del código. A continuación, discutiremos la cobertura de pruebas, las pruebas impulsadas por tablas y el uso de simulaciones para mejorar la eficiencia de las pruebas.
5.1 Cobertura de pruebas y herramientas relacionadas
La herramienta go test
proporciona una característica de cobertura de pruebas muy útil, que nos ayuda a comprender qué partes del código son cubiertas por los casos de prueba, descubriendo así áreas de código que no están cubiertas por los casos de prueba.
Usando el comando go test -cover
, puedes ver el porcentaje actual de cobertura de pruebas:
go test -cover
Si deseas comprender de forma más detallada qué líneas de código se ejecutaron y cuáles no, puedes utilizar el parámetro -coverprofile
, que genera un archivo de datos de cobertura. Luego, puedes usar el comando go tool cover
para generar un informe detallado de cobertura de pruebas.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
El comando anterior abrirá un informe web que muestra visualmente qué líneas de código están cubiertas por las pruebas y cuáles no. El color verde representa código cubierto por las pruebas, mientras que el rojo representa líneas de código no cubiertas.
5.2 Uso de Mocks
En pruebas, a menudo nos encontramos con escenarios donde necesitamos simular dependencias externas. Los mocks pueden ayudarnos a simular estas dependencias, eliminando la necesidad de depender de servicios externos específicos o recursos en el entorno de pruebas.
En la comunidad Go, existen muchas herramientas de mock, como testify/mock
y gomock
. Estas herramientas suelen proporcionar una serie de APIs para crear y usar objetos mock.
Aquí tienes un ejemplo básico de uso de testify/mock
. Lo primero que debes hacer es definir una interfaz y su versión de mock:
type DataService interface {
FetchData() (int, error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchData() (int, error) {
args := m.Called()
return args.Int(0), args.Error(1)
}
En las pruebas, podemos usar MockDataService
para reemplazar el servicio de datos real:
func TestAlgo(t *testing.T) {
mockDataSvc := new(MockDataService)
mockDataSvc.On("FetchData").Return(42, nil) // Configurando el comportamiento esperado
resultado, err := mockDataSvc.FetchData() // Usando el objeto mock
assert.NoError(t, err)
assert.Equal(t, 42, resultado)
mockDataSvc.AssertExpectations(t) // Verificando si se produjo el comportamiento esperado
}
A través del enfoque anterior, podemos evitar depender o aislar servicios externos, llamadas a base de datos, etc. en las pruebas. Esto puede acelerar la ejecución de las pruebas y hacer que nuestras pruebas sean más estables y confiables.
6. Técnicas Avanzadas de Pruebas
Después de dominar los conceptos básicos de las pruebas unitarias en Go, podemos explorar algunas técnicas de pruebas más avanzadas, las cuales ayudan a construir software más robusto y mejorar la eficiencia de las pruebas.
6.1 Pruebas de Funciones Privadas
En Golang, las funciones privadas suelen referirse a funciones no exportadas, es decir, funciones cuyos nombres comienzan con una letra minúscula. Por lo general, preferimos probar interfaces públicas, ya que reflejan la usabilidad del código. Sin embargo, hay casos en los que también tiene sentido probar directamente las funciones privadas, como cuando la función privada tiene lógica compleja y es llamada por múltiples funciones públicas.
Probar funciones privadas difiere de probar funciones públicas porque no se puede acceder a ellas desde fuera del paquete. Una técnica común es escribir el código de prueba en el mismo paquete, lo que permite acceder a la función privada.
Aquí tienes un ejemplo sencillo:
// calculator.go
package calculator
func add(a, b int) int {
return a + b
}
El archivo de prueba correspondiente es el siguiente:
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
esperado := 4
actual := add(2, 2)
if actual != esperado {
t.Errorf("esperado %d, obtuvo %d", esperado, actual)
}
}
Al colocar el archivo de prueba en el mismo paquete, podemos probar directamente la función add
.
6.2 Patrones Comunes de Pruebas y Mejores Prácticas
Las pruebas unitarias en Go tienen algunos patrones comunes que facilitan el trabajo de pruebas y ayudan a mantener la claridad y la mantenibilidad del código.
-
Pruebas Basadas en Tablas
Las pruebas basadas en tablas son un método de organizar las entradas de prueba y las salidas esperadas. Al definir un conjunto de casos de prueba y luego recorrerlos para realizar pruebas, este método facilita mucho la adición de nuevos casos de prueba y también hace que el código sea más fácil de leer y mantener.
// calculator_test.go
package calculator
import "testing"
func TestAddTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{1, 2, 3},
{2, 2, 4},
{5, -1, 4},
}
for _, tt := range tests {
nombrePrueba := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(nombrePrueba, func(t *testing.T) {
ans := add(tt.a, tt.b)
if tt.want != ans {
t.Errorf("obtuvo %d, esperaba %d", ans, tt.want)
}
})
}
}
-
Uso de Mocks para Pruebas
El mock es una técnica de prueba que implica reemplazar dependencias para probar varias partes de la funcionalidad. En Golang, las interfaces son la forma principal de implementar mocks. Al usar interfaces, se puede crear una implementación de mock y luego usarla en las pruebas.