Curso Go #9: Testing y Benchmarking en Go
Testing y Benchmarking en Go - Parte 9 de 10

Bienvenido al articulo 9 de 10 del Curso de Go desde Cero. Hasta ahora hemos aprendido a crear programas completos con funciones, structs, interfaces, goroutines y mas. Pero hay una pregunta que todo programador profesional debe responder: ¿como sabes que tu codigo realmente funciona?
La respuesta es testing. Go tiene soporte de primera clase para testing directamente en su biblioteca estandar, sin necesidad de instalar frameworks externos. En este articulo vas a aprender a escribir tests unitarios, tests de tabla, benchmarks y mucho mas.
¿Por que importa el testing?
Escribir tests no es opcional en el desarrollo profesional. Los tests te dan confianza para:
- Refactorizar sin miedo a romper funcionalidad existente
- Documentar el comportamiento esperado de tu codigo
- Detectar bugs antes de que lleguen a produccion
- Colaborar con otros desarrolladores con seguridad
Go fue disenado desde el principio con testing en mente. El paquete testing viene incluido en la biblioteca estandar y el comando go test esta integrado en la herramienta de Go. No necesitas nada externo para empezar.
El paquete testing y las convenciones
Go tiene convenciones estrictas para los archivos de test:
- Los archivos de test deben terminar en
_test.go - Los archivos de test van en el mismo paquete que el codigo que prueban
- Las funciones de test empiezan con
Testseguido de un nombre descriptivo con la primera letra en mayuscula - Cada funcion de test recibe un parametro
*testing.T
Supongamos que tienes un archivo math.go con funciones matematicas:
1// math.go
2package mathutil
3
4// Sumar retorna la suma de dos enteros.
5func Sumar(a, b int) int {
6 return a + b
7}
8
9// Factorial calcula el factorial de un numero no negativo.
10func Factorial(n int) int {
11 if n <= 1 {
12 return 1
13 }
14 return n * Factorial(n-1)
15}
16
17// EsPrimo verifica si un numero es primo.
18func EsPrimo(n int) bool {
19 if n < 2 {
20 return false
21 }
22 for i := 2; i*i <= n; i++ {
23 if n%i == 0 {
24 return false
25 }
26 }
27 return true
28}
El archivo de test correspondiente seria math_test.go:
1// math_test.go
2package mathutil
3
4import "testing"
5
6func TestSumar(t *testing.T) {
7 resultado := Sumar(2, 3)
8 if resultado != 5 {
9 t.Errorf("Sumar(2, 3) = %d; esperado 5", resultado)
10 }
11}
12
13func TestFactorial(t *testing.T) {
14 resultado := Factorial(5)
15 if resultado != 120 {
16 t.Errorf("Factorial(5) = %d; esperado 120", resultado)
17 }
18}
19
20func TestEsPrimo(t *testing.T) {
21 if !EsPrimo(7) {
22 t.Error("EsPrimo(7) deberia ser true")
23 }
24 if EsPrimo(4) {
25 t.Error("EsPrimo(4) deberia ser false")
26 }
27}
TestSumar, TestFactorialConCero, TestEsPrimoConNumerosNegativos. Esto hace que sea facil identificar que fallo cuando un test no pasa.
Table-Driven Tests: el patron idiomatico de Go
Uno de los patrones mas importantes y comunes en Go es el table-driven test. En lugar de escribir una funcion de test por cada caso, defines una tabla (slice de structs) con todos los casos y los recorres en un loop:
1func TestSumarTabla(t *testing.T) {
2 tests := []struct {
3 nombre string
4 a, b int
5 esperado int
6 }{
7 {"positivos", 2, 3, 5},
8 {"con cero", 0, 5, 5},
9 {"negativos", -1, -2, -3},
10 {"mixto", -1, 5, 4},
11 {"grandes", 1000000, 2000000, 3000000},
12 }
13
14 for _, tt := range tests {
15 t.Run(tt.nombre, func(t *testing.T) {
16 resultado := Sumar(tt.a, tt.b)
17 if resultado != tt.esperado {
18 t.Errorf("Sumar(%d, %d) = %d; esperado %d",
19 tt.a, tt.b, resultado, tt.esperado)
20 }
21 })
22 }
23}
24
25func TestEsPrimoTabla(t *testing.T) {
26 tests := []struct {
27 nombre string
28 entrada int
29 esperado bool
30 }{
31 {"negativo", -1, false},
32 {"cero", 0, false},
33 {"uno", 1, false},
34 {"dos", 2, true},
35 {"tres", 3, true},
36 {"cuatro", 4, false},
37 {"siete", 7, true},
38 {"diez", 10, false},
39 {"trece", 13, true},
40 }
41
42 for _, tt := range tests {
43 t.Run(tt.nombre, func(t *testing.T) {
44 resultado := EsPrimo(tt.entrada)
45 if resultado != tt.esperado {
46 t.Errorf("EsPrimo(%d) = %v; esperado %v",
47 tt.entrada, resultado, tt.esperado)
48 }
49 })
50 }
51}
t.Run crea subtests. Cada subtest tiene su propio nombre y puede ejecutarse de forma independiente. Si un subtest falla, puedes ver exactamente cual caso fallo sin revisar todos los demas. Puedes ejecutar un subtest especifico con go test -run TestEsPrimoTabla/siete.
Helpers y funciones auxiliares en tests
Cuando tienes logica repetida en tus tests, puedes extraerla a funciones helper. Go proporciona t.Helper() para marcar una funcion como auxiliar, lo que mejora los mensajes de error:
1func assertEqual(t *testing.T, got, want int) {
2 t.Helper() // marca esta funcion como helper
3 if got != want {
4 t.Errorf("got %d, want %d", got, want)
5 }
6}
7
8func TestConHelper(t *testing.T) {
9 assertEqual(t, Sumar(1, 2), 3)
10 assertEqual(t, Sumar(0, 0), 0)
11 assertEqual(t, Sumar(-1, 1), 0)
12}
Sin t.Helper(), el mensaje de error mostraria la linea dentro de assertEqual. Con t.Helper(), el mensaje apunta a la linea donde se llamo a assertEqual en el test, lo cual es mucho mas util.
t.Fatal vs t.Error
Hay una diferencia importante entre los metodos de reporte:
t.Error()yt.Errorf(): reportan el fallo pero continuan ejecutando el testt.Fatal()yt.Fatalf(): reportan el fallo y detienen el test inmediatamente
1func TestConFatal(t *testing.T) {
2 resultado := ObtenerConexion()
3 if resultado == nil {
4 t.Fatal("La conexion es nil, no se puede continuar")
5 // El test se detiene aqui
6 }
7 // Este codigo solo se ejecuta si resultado no es nil
8 resultado.Ejecutar()
9}
t.Fatal cuando una falla hace imposible continuar con el resto del test (por ejemplo, si un recurso necesario es nil). Usa t.Error cuando quieres reportar el fallo pero seguir verificando otros aspectos.
Cobertura de tests
Go incluye herramientas integradas para medir la cobertura de tus tests. Esto te dice que porcentaje de tu codigo esta siendo ejecutado por tus tests:
1# Ver porcentaje de cobertura en la terminal
2go test -cover ./...
3
4# Generar un perfil de cobertura detallado
5go test -coverprofile=coverage.out ./...
6
7# Ver el reporte en HTML (abre en el navegador)
8go tool cover -html=coverage.out
9
10# Ver cobertura por funcion
11go tool cover -func=coverage.out
La salida del comando go test -cover se ve asi:
1ok mathutil 0.003s coverage: 85.7% of statements
El reporte HTML es especialmente util porque resalta en verde las lineas cubiertas y en rojo las que no estan cubiertas por ningun test.
Benchmarking: midiendo el rendimiento
Go permite escribir benchmarks junto con tus tests. Los benchmarks miden cuanto tiempo toma ejecutar una funcion, lo cual es util para optimizar codigo critico:
1func BenchmarkFactorial(b *testing.B) {
2 for i := 0; i < b.N; i++ {
3 Factorial(20)
4 }
5}
6
7func BenchmarkEsPrimo(b *testing.B) {
8 for i := 0; i < b.N; i++ {
9 EsPrimo(7919) // un numero primo grande
10 }
11}
12
13func BenchmarkSumar(b *testing.B) {
14 for i := 0; i < b.N; i++ {
15 Sumar(1000, 2000)
16 }
17}
Para ejecutar los benchmarks:
1# Ejecutar todos los benchmarks
2go test -bench=. ./...
3
4# Ejecutar un benchmark especifico
5go test -bench=BenchmarkFactorial ./...
6
7# Incluir estadisticas de memoria
8go test -bench=. -benchmem ./...
La salida se ve algo asi:
1BenchmarkFactorial-8 5000000 230 ns/op 0 B/op 0 allocs/op
2BenchmarkEsPrimo-8 10000000 105 ns/op 0 B/op 0 allocs/op
Esto te dice que Factorial(20) tarda aproximadamente 230 nanosegundos por operacion y no hace asignaciones de memoria.
Example Functions: documentacion ejecutable
Go tiene un tipo especial de test llamado Example. Estas funciones sirven como documentacion que se puede verificar automaticamente:
1func ExampleSumar() {
2 fmt.Println(Sumar(2, 3))
3 fmt.Println(Sumar(-1, 1))
4 // Output:
5 // 5
6 // 0
7}
8
9func ExampleFactorial() {
10 fmt.Println(Factorial(0))
11 fmt.Println(Factorial(5))
12 fmt.Println(Factorial(10))
13 // Output:
14 // 1
15 // 120
16 // 3628800
17}
El comentario // Output: al final es clave. Go ejecuta la funcion y compara la salida real con la esperada. Si no coinciden, el test falla. Ademas, estos ejemplos aparecen automaticamente en la documentacion generada por go doc.
Testing HTTP Handlers con httptest
El paquete net/http/httptest permite probar handlers HTTP sin levantar un servidor real:
1package main
2
3import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8)
9
10func SaludoHandler(w http.ResponseWriter, r *http.Request) {
11 w.Header().Set("Content-Type", "application/json")
12 json.NewEncoder(w).Encode(map[string]string{
13 "mensaje": "Hola, mundo",
14 })
15}
16
17func TestSaludoHandler(t *testing.T) {
18 // Crear un request de prueba
19 req := httptest.NewRequest(http.MethodGet, "/saludo", nil)
20 // Crear un ResponseRecorder para capturar la respuesta
21 rec := httptest.NewRecorder()
22
23 // Ejecutar el handler
24 SaludoHandler(rec, req)
25
26 // Verificar el status code
27 if rec.Code != http.StatusOK {
28 t.Errorf("status = %d; esperado %d", rec.Code, http.StatusOK)
29 }
30
31 // Verificar el Content-Type
32 ct := rec.Header().Get("Content-Type")
33 if ct != "application/json" {
34 t.Errorf("Content-Type = %s; esperado application/json", ct)
35 }
36
37 // Verificar el body
38 var body map[string]string
39 json.NewDecoder(rec.Body).Decode(&body)
40 if body["mensaje"] != "Hola, mundo" {
41 t.Errorf("mensaje = %s; esperado Hola, mundo", body["mensaje"])
42 }
43}
httptest.NewRecorder() implementa http.ResponseWriter, asi que puedes pasarlo directamente a cualquier handler. Esto hace que sea muy facil probar endpoints HTTP sin necesidad de un servidor real.
Mocking con interfaces
Go no necesita un framework de mocking complejo. Gracias a las interfaces, puedes crear mocks de forma sencilla y natural:
1// Definir una interfaz para el servicio
2type NotificadorEmail interface {
3 Enviar(destino, asunto, cuerpo string) error
4}
5
6// Implementacion real (para produccion)
7type SMTPNotificador struct {
8 Host string
9}
10
11func (s *SMTPNotificador) Enviar(destino, asunto, cuerpo string) error {
12 // Logica real de envio de email...
13 return nil
14}
15
16// Mock para testing
17type MockNotificador struct {
18 LlamadoConDestino string
19 LlamadoConAsunto string
20 ErrorARetornar error
21}
22
23func (m *MockNotificador) Enviar(destino, asunto, cuerpo string) error {
24 m.LlamadoConDestino = destino
25 m.LlamadoConAsunto = asunto
26 return m.ErrorARetornar
27}
28
29// Servicio que usa la interfaz
30type ServicioUsuario struct {
31 Notificador NotificadorEmail
32}
33
34func (s *ServicioUsuario) Registrar(nombre, email string) error {
35 // ... logica de registro ...
36 return s.Notificador.Enviar(email, "Bienvenido", "Hola "+nombre)
37}
38
39// Test usando el mock
40func TestRegistrarUsuario(t *testing.T) {
41 mock := &MockNotificador{}
42 servicio := &ServicioUsuario{Notificador: mock}
43
44 err := servicio.Registrar("Ana", "[email protected]")
45 if err != nil {
46 t.Fatalf("error inesperado: %v", err)
47 }
48 if mock.LlamadoConDestino != "[email protected]" {
49 t.Errorf("destino = %s; esperado [email protected]",
50 mock.LlamadoConDestino)
51 }
52 if mock.LlamadoConAsunto != "Bienvenido" {
53 t.Errorf("asunto = %s; esperado Bienvenido",
54 mock.LlamadoConAsunto)
55 }
56}
Ejecutando tests: comandos esenciales
Aqui tienes un resumen de los comandos mas utiles para ejecutar tests en Go:
1# Ejecutar todos los tests del paquete actual
2go test
3
4# Ejecutar todos los tests de todos los paquetes
5go test ./...
6
7# Modo verbose (muestra el nombre de cada test)
8go test -v ./...
9
10# Ejecutar solo tests que coincidan con un patron
11go test -run TestSumar ./...
12
13# Ejecutar un subtest especifico
14go test -run TestSumarTabla/positivos ./...
15
16# Ejecutar tests con cobertura
17go test -cover ./...
18
19# Ejecutar benchmarks (no ejecuta tests normales)
20go test -bench=. -run=^$ ./...
21
22# Ejecutar tests con limite de tiempo
23go test -timeout 30s ./...
24
25# Ejecutar tests en modo race detector
26go test -race ./...
-race es especialmente importante si usas goroutines. Detecta condiciones de carrera (race conditions) que podrian causar bugs intermitentes y dificiles de reproducir. Usalo siempre en tu pipeline de CI/CD.
Resumen y proximo articulo
En este articulo aprendimos todo sobre testing y benchmarking en Go:
- Las convenciones de Go para archivos y funciones de test
- Como escribir tests unitarios con el paquete
testing - El patron table-driven tests, el mas idiomatico de Go
- Subtests con
t.Runpara organizar y ejecutar casos individuales - Helpers con
t.Helper()para mensajes de error claros - Cobertura con
go test -covery reportes HTML - Benchmarks con
BenchmarkXxxpara medir rendimiento - Example functions como documentacion ejecutable
- Testing de HTTP handlers con
httptest - Mocking con interfaces para aislar dependencias
En el proximo y ultimo articulo (Parte 10 de 10) vamos a construir un proyecto final completo: una API REST para gestionar tareas usando todo lo que hemos aprendido en el curso. ¡Preparate para el gran final!
Comments
Sign in to leave a comment
No comments yet. Be the first!