En Go, el testing no es un añadido externo, sino una parte integral del ciclo de vida de desarrollo. Cuando escribes código, el compilador y el comando go test colaboran para asegurar que tus pruebas no afecten la producción. Los archivos que terminan en *_test.go tienen una propiedad fundamental: el compilador los ignora completamente cuando ejecutas go build o go run, pero los incluye cuando ejecutas go test. Esto permite que tus pruebas residan en el mismo paquete que el código que prueban, facilitando el acceso a miembros no exportados sin ensuciar el binario final.
Esta arquitectura se basa en una filosofía de simplicidad y estabilidad. A diferencia de otros lenguajes donde la comunidad salta constantemente entre frameworks como Jest, Mocha o Pytest, Go apuesta por su biblioteca estándar. La razón es clara: un conjunto de herramientas unificado evita la fragmentación y asegura que el código de prueba de hace cinco años siga funcionando exactamente igual hoy. No necesitas una DSL (Domain Specific Language) compleja; necesitas una función que reciba un *testing.T.
Para que el runner de Go reconozca una función como un test, debe cumplir una convención estricta: debe empezar con el prefijo Test seguido de una letra mayúscula y recibir un único parámetro de tipo *testing.T. Si intentas ejecutar go test ./..., el comando recorrerá todos los subdirectorios de tu módulo buscando estos archivos y funciones.
Sin embargo, la gestión de fallos requiere que entiendas la diferencia entre un error “suave” y uno “fatal”. Si usas t.Error (o su variante con formato t.Errorf), marcas el test como fallido pero permites que la ejecución continúe. Esto es ideal cuando quieres verificar múltiples condiciones independientes en un mismo test. Por el contrario, t.Fatal (o t.Fatalf) detiene la ejecución de la función de test actual en ese mismo instante. Si un error es tan crítico que cualquier paso siguiente causaría un pánico o un comportamiento errático, debes usar la versión Fatal. Finalmente, para depuración, t.Log es tu aliado: sus mensajes solo se muestran en la terminal si el test falla o si ejecutas el comando con la bandera -v.
Si intentas incluir los tests en los archivos principales sin usar el sufijo _test.go, terminarás inflando el binario de producción con código de validación. Además, si no gestionas correctamente la interrupción del test con t.Fatal cuando un error es crítico, el test continuará ejecutándose en un estado inválido, lo que puede provocar un pánico y ocultar la verdadera causa del fallo.
package main
import (
"errors"
"fmt"
"testing"
)
// Divide realiza una división de dos números flotantes.
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divisor es cero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
// Caso 1: División por cero (Error crítico)
// Usamos Fatal porque si el error es nulo, el test sigue
// con una lógica que no tiene sentido para este escenario.
res, err := Divide(10, 0)
if err != nil {
t.Fatalf("Error crítico esperado: %v", err)
}
// Esta línea nunca se alcanza si el error es detectado correctamente.
if res != 0 {
t.Errorf("Se esperaba 0, se obtuvo %f", res)
}
// Caso 2: División normal (Validación de valor)
// Usamos Error porque si el valor es incorrecto, queremos saberlo
// pero el test puede seguir ejecutando otras comprobaciones.
res, err = Divide(10, 2)
if err != nil {
t.Errorf("Error inesperado en división normal: %v", err)
}
if res != 5 {
t.Errorf("Resultado esperado 5, obtenido %f", res)
}
// t.Log solo es visible si ejecutas `go test -v` o si el test falla.
t.Log("Validación de división completada con éxito")
}
func main() {
fmt.Println("Ejecuta `go test -v` para ver los tests en acción")
}
Al observar el TestDivide, verás que para el primer caso usamos t.Fatalf. Esto es crucial porque si la división por cero no devolviera el error esperado, el test intentaría evaluar el valor de res, lo cual sería un error de lógica en la prueba. En cambio, para la división normal, usamos t.Errorf. Esto nos permite reportar un error en el valor de res pero permite que el test continúe su ejecución, lo cual es útil si tuviéramos más aserciones después. El uso de t.Log permite dejar notas de depuración que no ensucian la salida estándar en ejecuciones normales, pero que aportan contexto vital cuando algo falla.
El error frecuente
// Error: Uso incorrecto de t.Error en un escenario de dependencia.
func TestErrorDeDependencia(t *testing.T) {
// Supongamos que esto inicializa un recurso crítico.
conn, err := conectarBaseDeDatos()
if err != nil {
// t.Error NO detiene la ejecución del test.
t.Error("Error al conectar a la base de datos")
}
// Si err != nil, conn es nil. La siguiente línea causará un PANIC.
// El error original se pierde entre el pánico de la siguiente línea.
err = conn.Ping()
if err != nil {
t.Error("Ping falló")
}
}
Dominar la distinción entre error y fatal es lo que separa un test que diagnostica problemas de uno que simplemente colapsa.
N° 152