Las pruebas unitarias son pruebas automatizadas que validan el comportamiento de pequeñas unidades de código, como funciones o métodos. El objetivo es asegurar que cada unidad funcione correctamente en aislamiento, detectando errores de forma temprana en el ciclo de desarrollo.
Go incluye herramientas integradas para escribir y ejecutar pruebas. Para comenzar, se debe crear un archivo de pruebas con el sufijo _test.go
. Este archivo contiene funciones de prueba que utilizan el paquete testing
.
A continuación, se muestra un ejemplo básico de una prueba unitaria en Go:
// archivo: math_test.go
package main
import (
"testing"
)
// Función a probar
func Sum(a, b int) int {
return a + b
}
// Función de prueba
func TestSum(t *testing.T) {
result := Sum(2, 3)
expected := 5
if result != expected {
t.Errorf("Sum(2, 3) = %d; expected %d", result, expected)
}
}
En este ejemplo, Sum
es la función a probar y TestSum
es la función de prueba. La función de prueba compara el resultado de Sum
con el valor esperado y usa t.Errorf
para reportar un error si los valores no coinciden.
Para ejecutar las pruebas, utiliza el comando go test
en el directorio que contiene los archivos de prueba:
$ go test
Este comando buscará automáticamente todos los archivos _test.go
y ejecutará las funciones de prueba que comienzan con Test
.
Las tablas de prueba son una técnica común en Go para organizar múltiples casos de prueba en una sola función de prueba. Esto permite probar varios escenarios de manera concisa y clara.
// archivo: math_test.go
package main
import (
"testing"
)
// Función a probar
func Multiply(a, b int) int {
return a * b
}
// Función de prueba con tabla de prueba
func TestMultiply(t *testing.T) {
var tests = []struct {
a, b, expected int
}{
{2, 3, 6},
{4, 5, 20},
{0, 1, 0},
{7, 8, 56},
}
for _, tt := range tests {
result := Multiply(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Multiply(%d, %d) = %d; expected %d", tt.a, tt.b, result, tt.expected)
}
}
}
En este ejemplo, la función TestMultiply
define una tabla de prueba con varios casos. Luego, recorre la tabla y ejecuta la función Multiply
para cada caso, verificando si el resultado coincide con el valor esperado.
Es común que las funciones en Go retornen errores. Las pruebas unitarias deben verificar tanto los resultados como los errores.
// archivo: divide_test.go
package main
import (
"errors"
"testing"
)
// Función a probar
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Función de prueba para manejo de errores
func TestDivide(t *testing.T) {
result, err := Divide(4, 2)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
expected := 2.0
if result != expected {
t.Errorf("Divide(4, 2) = %f; expected %f", result, expected)
}
_, err = Divide(4, 0)
if err == nil {
t.Error("expected an error but got nil")
}
}
En este ejemplo, TestDivide
prueba la función Divide
, verificando tanto el resultado de la división como el manejo del error cuando el divisor es cero.
Las pruebas unitarias también pueden probar métodos de estructuras. Es útil para probar el comportamiento de métodos asociados a tipos definidos por el usuario.
// archivo: rectangle_test.go
package main
import (
"testing"
)
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func TestRectangleArea(t *testing.T) {
rect := Rectangle{Width: 3, Height: 4}
result := rect.Area()
expected := 12.0
if result != expected {
t.Errorf("Rectangle.Area() = %f; expected %f", result, expected)
}
}
En este ejemplo, TestRectangleArea
prueba el método Area
de la estructura Rectangle
.
t.Run
para Subpruebas
Go permite definir subpruebas dentro de una función de prueba utilizando t.Run
. Esto es útil para organizar mejor las pruebas y proporcionar salidas de prueba más claras.
// archivo: math_test.go
package main
import (
"testing"
)
func TestOperations(t *testing.T) {
t.Run("Sum", func(t *testing.T) {
result := Sum(2, 3)
expected := 5
if result != expected {
t.Errorf("Sum(2, 3) = %d; expected %d", result, expected)
}
})
t.Run("Multiply", func(t *testing.T) {
result := Multiply(2, 3)
expected := 6
if result != expected {
t.Errorf("Multiply(2, 3) = %d; expected %d", result, expected)
}
})
}
En este ejemplo, TestOperations
define subpruebas para Sum
y Multiply
, proporcionando una mejor organización y claridad en las salidas de prueba.
Go tiene soporte nativo para concurrencia. Es crucial probar correctamente el código concurrente para asegurarse de que funcione correctamente en situaciones multi-hilo.
// archivo: concurrent_test.go
package main
import (
"sync"
"testing"
)
func TestConcurrentIncrement(t *testing.T) {
var counter int
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
counter++
}()
}
wg.Wait()
if counter != 1000 {
t.Errorf("counter = %d; expected 1000", counter)
}
}
En este ejemplo, TestConcurrentIncrement
prueba una operación de incremento concurrente utilizando un mutex para asegurar que las operaciones sean seguras en concurrencia.
La herramienta go test
también puede medir la cobertura de código, indicando qué partes del código son ejecutadas por las pruebas.
$ go test -cover
Para generar un informe de cobertura más detallado:
$ go test -coverprofile=coverage.out
$ go tool cover -html=coverage.out
Este comando genera un archivo de informe de cobertura y lo presenta en un formato HTML.
Las pruebas unitarias en Go son esenciales para asegurar la calidad y fiabilidad del código. Gracias a su soporte integrado para pruebas y su enfoque en la simplicidad, Go facilita la escritura y ejecución de pruebas unitarias. Desde pruebas básicas hasta técnicas avanzadas como tablas de prueba y subpruebas, Go proporciona las herramientas necesarias para mantener un alto estándar de calidad en el desarrollo de software.
Jorge García
Fullstack developer