← Volver al listado de tecnologías

Testing en Go

Por: Artiko
gotestingbenchmarkcoverage

Testing en Go

Go incluye un framework de testing completo en su stdlib. No necesitas dependencias externas para escribir tests robustos.

Estructura basica

Los tests viven en archivos _test.go junto al codigo que prueban.

mipackage/
  calculator.go
  calculator_test.go
// calculator.go
package calculator

func Sumar(a, b int) int {
    return a + b
}

func Dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division por cero")
    }
    return a / b, nil
}
// calculator_test.go
package calculator

import "testing"

func TestSumar(t *testing.T) {
    resultado := Sumar(2, 3)
    if resultado != 5 {
        t.Errorf("Sumar(2, 3) = %d; esperado 5", resultado)
    }
}

Comandos de go test

go test              # Ejecuta tests del paquete actual
go test ./...        # Ejecuta tests de todos los subpaquetes
go test -v           # Modo verbose (muestra cada test)
go test -run TestSum # Filtra tests por nombre (regex)
go test -count=1     # Desactiva cache de tests

Metodos de t *testing.T

MetodoDescripcion
t.Error(args...)Reporta error, continua ejecucion
t.Errorf(format, args...)Error con formato, continua
t.Fatal(args...)Reporta error, detiene el test
t.Fatalf(format, args...)Fatal con formato
t.Log(args...)Log visible con -v
t.Skip(args...)Salta el test
t.Helper()Marca funcion como helper (mejor stack trace)
func TestDividir(t *testing.T) {
    _, err := Dividir(10, 0)
    if err == nil {
        t.Fatal("esperaba error por division por cero")
    }

    resultado, err := Dividir(10, 2)
    if err != nil {
        t.Fatalf("error inesperado: %v", err)
    }
    if resultado != 5.0 {
        t.Errorf("Dividir(10, 2) = %f; esperado 5.0", resultado)
    }
}

Table-driven tests

Patron idiomatico de Go para probar multiples escenarios de forma concisa.

package calculator

import "testing"

func TestSumarTabla(t *testing.T) {
    tests := []struct {
        nombre   string
        a, b     int
        esperado int
    }{
        {"positivos", 2, 3, 5},
        {"negativos", -1, -1, -2},
        {"con cero", 5, 0, 5},
        {"grandes", 1000000, 1, 1000001},
    }

    for _, tt := range tests {
        t.Run(tt.nombre, func(t *testing.T) {
            resultado := Sumar(tt.a, tt.b)
            if resultado != tt.esperado {
                t.Errorf("Sumar(%d, %d) = %d; esperado %d",
                    tt.a, tt.b, resultado, tt.esperado)
            }
        })
    }
}
go test -v -run TestSumarTabla
# === RUN   TestSumarTabla
# === RUN   TestSumarTabla/positivos
# === RUN   TestSumarTabla/negativos
# === RUN   TestSumarTabla/con_cero
# === RUN   TestSumarTabla/grandes
# --- PASS: TestSumarTabla

Table-driven con errores

func TestDividirTabla(t *testing.T) {
    tests := []struct {
        nombre    string
        a, b      float64
        esperado  float64
        expectErr bool
    }{
        {"normal", 10, 2, 5.0, false},
        {"decimal", 7, 3, 2.3333333333333335, false},
        {"division por cero", 10, 0, 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.nombre, func(t *testing.T) {
            resultado, err := Dividir(tt.a, tt.b)
            if tt.expectErr {
                if err == nil {
                    t.Error("esperaba error, no llego")
                }
                return
            }
            if err != nil {
                t.Fatalf("error inesperado: %v", err)
            }
            if resultado != tt.esperado {
                t.Errorf("got %f; want %f", resultado, tt.esperado)
            }
        })
    }
}

Subtests con t.Run

t.Run() permite agrupar tests logicamente y ejecutarlos por separado.

func TestOperaciones(t *testing.T) {
    t.Run("suma", func(t *testing.T) {
        if Sumar(1, 1) != 2 {
            t.Error("1 + 1 deberia ser 2")
        }
    })

    t.Run("grupo/division", func(t *testing.T) {
        res, _ := Dividir(10, 5)
        if res != 2.0 {
            t.Error("10 / 5 deberia ser 2")
        }
    })
}
# Ejecutar solo el subtest de suma
go test -run TestOperaciones/suma

Tests paralelos

func TestParalelo(t *testing.T) {
    tests := []struct {
        nombre string
        input  int
        expect int
    }{
        {"caso1", 1, 2},
        {"caso2", 2, 4},
        {"caso3", 3, 6},
    }

    for _, tt := range tests {
        t.Run(tt.nombre, func(t *testing.T) {
            t.Parallel() // Marca el subtest como paralelo
            resultado := tt.input * 2
            if resultado != tt.expect {
                t.Errorf("got %d; want %d", resultado, tt.expect)
            }
        })
    }
}

Importante: Con t.Parallel() los subtests corren concurrentemente. Asegurate de no compartir estado mutable entre ellos.

Funciones helper

func assertEqual(t *testing.T, got, want int) {
    t.Helper() // El error reporta la linea del caller, no del helper
    if got != want {
        t.Errorf("got %d; want %d", got, want)
    }
}

func TestConHelper(t *testing.T) {
    assertEqual(t, Sumar(2, 2), 4)
    assertEqual(t, Sumar(0, 0), 0)
}

TestMain

Permite setup y teardown global para todos los tests de un paquete.

package mipackage

import (
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    // Setup global
    setup()

    // Ejecuta todos los tests
    code := m.Run()

    // Teardown global
    teardown()

    os.Exit(code)
}

func setup()    { /* inicializar DB, etc */ }
func teardown() { /* limpiar recursos */ }

Benchmarks

Miden el rendimiento de funciones. El nombre debe empezar con Benchmark.

package calculator

import "testing"

func BenchmarkSumar(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Sumar(100, 200)
    }
}

func BenchmarkDividir(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Dividir(100, 3)
    }
}
go test -bench=.
# BenchmarkSumar-8     1000000000    0.2500 ns/op
# BenchmarkDividir-8    500000000    2.340  ns/op

go test -bench=BenchmarkSumar -benchmem
# BenchmarkSumar-8  1000000000  0.25 ns/op  0 B/op  0 allocs/op

go test -bench=. -benchtime=5s  # Ejecutar por 5 segundos
go test -bench=. -count=3       # Repetir 3 veces
FlagDescripcion
-bench=.Ejecuta todos los benchmarks
-benchmemMuestra allocaciones de memoria
-benchtime=5sDuracion del benchmark
-count=3Repeticiones para promediar

Coverage

go test -cover                     # Muestra porcentaje de cobertura
go test -coverprofile=cover.out    # Genera archivo de cobertura
go tool cover -html=cover.out      # Visualiza en navegador
go tool cover -func=cover.out      # Muestra cobertura por funcion

Ejemplo de salida:

ok   mipackage  0.003s  coverage: 85.7% of statements

Mocks con interfaces

Go usa interfaces para inyeccion de dependencias, facilitando los mocks sin librerias externas.

// servicio.go
package user

type UsuarioRepo interface {
    ObtenerPorID(id int) (*Usuario, error)
}

type Usuario struct {
    ID     int
    Nombre string
}

type ServicioUsuario struct {
    repo UsuarioRepo
}

func NuevoServicio(repo UsuarioRepo) *ServicioUsuario {
    return &ServicioUsuario{repo: repo}
}

func (s *ServicioUsuario) ObtenerNombre(id int) (string, error) {
    u, err := s.repo.ObtenerPorID(id)
    if err != nil {
        return "", err
    }
    return u.Nombre, nil
}
// servicio_test.go
package user

import (
    "fmt"
    "testing"
)

type mockRepo struct {
    usuarios map[int]*Usuario
}

func (m *mockRepo) ObtenerPorID(id int) (*Usuario, error) {
    u, ok := m.usuarios[id]
    if !ok {
        return nil, fmt.Errorf("usuario %d no encontrado", id)
    }
    return u, nil
}

func TestObtenerNombre(t *testing.T) {
    mock := &mockRepo{
        usuarios: map[int]*Usuario{
            1: {ID: 1, Nombre: "Ana"},
        },
    }
    svc := NuevoServicio(mock)

    tests := []struct {
        nombre    string
        id        int
        esperado  string
        expectErr bool
    }{
        {"existente", 1, "Ana", false},
        {"no existe", 99, "", true},
    }

    for _, tt := range tests {
        t.Run(tt.nombre, func(t *testing.T) {
            nombre, err := svc.ObtenerNombre(tt.id)
            if tt.expectErr {
                if err == nil {
                    t.Error("esperaba error")
                }
                return
            }
            if err != nil {
                t.Fatalf("error inesperado: %v", err)
            }
            if nombre != tt.esperado {
                t.Errorf("got %q; want %q", nombre, tt.esperado)
            }
        })
    }
}

Mencion: testify

Para proyectos grandes, testify ofrece assertions mas legibles:

go get github.com/stretchr/testify
import "github.com/stretchr/testify/assert"

func TestConTestify(t *testing.T) {
    assert.Equal(t, 5, Sumar(2, 3))
    assert.NotNil(t, resultado)
    assert.NoError(t, err)
}

La stdlib es suficiente para la mayoria de proyectos. testify es opcional y agrega dependencias.


← Cap 17: Sync y Context | Cap 19: I/O, OS y Archivos →