Testing en Go 2026: Tests Unitarios, Mocks y Preguntas de Entrevista Técnica

Guía completa sobre testing en Go: escritura de tests unitarios con el paquete testing, creación de mocks con gomock, tests basados en tablas y preparación para entrevistas técnicas.

Tests unitarios y mocks en Go

El framework de testing de Go se basa en el paquete estándar testing, incluido en cada instalación de Go sin dependencias externas. A diferencia de frameworks en otros lenguajes, el enfoque de Go privilegia la simplicidad: los archivos de test conviven con el código de producción, las funciones de test siguen una convención de nomenclatura, y el comando go test maneja el descubrimiento y la ejecución.

Lo que buscan los entrevistadores

Candidatos que escriben tests basados en tablas, utilizan interfaces para inyección de dependencias y comprenden cuándo el mocking aporta valor versus cuándo añade ruido.

Escritura de Tests Unitarios con el Paquete testing

Cada archivo de test termina con _test.go y pertenece al mismo paquete que el código que prueba. Las funciones de test comienzan con Test seguido de un nombre en mayúscula. El parámetro *testing.T proporciona métodos para reportar fallos.

calculator_test.gogo
package calculator

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

La ejecución de go test ./... lanza todos los tests del módulo actual. El flag -v muestra los nombres de los tests individuales y su estado.

Tests Basados en Tablas: El Estándar de Go

Los tests basados en tablas reducen la duplicación y hacen trivial agregar nuevos casos. Cada fila de la tabla representa un escenario con sus entradas y salidas esperadas. Este patrón aparece en la biblioteca estándar de Go y en bases de código en producción.

validator_test.gogo
package validator

import "testing"

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"missing at sign", "userexample.com", true},
        {"empty string", "", true},
        {"unicode local part", "user@例え.jp", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
                    tt.email, err, tt.wantErr)
            }
        })
    }
}

El método t.Run crea subtests que se ejecutan independientemente. Los subtests fallidos indican qué caso específico falló sin detener los demás casos.

Inyección de Dependencias mediante Interfaces

Las interfaces de Go permiten probar código que depende de sistemas externos: bases de datos, clientes HTTP, sistemas de archivos. Definir una interfaz mínima en el punto de uso permite sustituir implementaciones reales por dobles de test.

service.gogo
package order

// Repository defines the data access methods this service needs
type Repository interface {
    FindByID(id string) (*Order, error)
    Save(order *Order) error
}

// Service handles order business logic
type Service struct {
    repo Repository
}

// NewService creates a service with the given repository
func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

// Process validates and saves an order
func (s *Service) Process(order *Order) error {
    if order.Total <= 0 {
        return ErrInvalidTotal
    }
    return s.repo.Save(order)
}

El Service acepta cualquier tipo que satisfaga Repository. El código de producción pasa una implementación conectada a la base de datos. Los tests pasan un mock o stub.

Mocking con gomock y mockgen

El paquete gomock genera implementaciones mock a partir de definiciones de interface. El comando mockgen lee archivos fuente y produce structs mock que registran llamadas y retornan valores configurados.

bash
# Generate mocks for the Repository interface
mockgen -source=service.go -destination=mocks/repository_mock.go -package=mocks

Uso del mock generado en tests:

service_test.gogo
package order

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    "yourmodule/order/mocks"
)

func TestService_Process_SavesValidOrder(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    order := &Order{ID: "123", Total: 99.99}

    // Expect Save to be called once with this order, return nil
    mockRepo.EXPECT().
        Save(order).
        Return(nil).
        Times(1)

    svc := NewService(mockRepo)
    err := svc.Process(order)

    assert.NoError(t, err)
}

func TestService_Process_RejectsZeroTotal(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    // No calls expected to repo since validation fails first

    svc := NewService(mockRepo)
    err := svc.Process(&Order{ID: "456", Total: 0})

    assert.ErrorIs(t, err, ErrInvalidTotal)
}

El mock verifica que Save fue llamado exactamente una vez con el argumento esperado. Si el código bajo prueba nunca llama a Save, o lo llama con argumentos diferentes, el test falla.

Uso de testify para Assertions y Suites

testify proporciona funciones de assertion que producen mensajes de fallo más claros que las verificaciones manuales con if. El paquete assert continúa la ejecución después de los fallos. El paquete require detiene el test inmediatamente.

user_test.gogo
package user

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestParseUser(t *testing.T) {
    input := `{"id": "u1", "name": "Alice"}`

    user, err := ParseUser(input)

    require.NoError(t, err, "parsing should not fail")
    assert.Equal(t, "u1", user.ID)
    assert.Equal(t, "Alice", user.Name)
    assert.Empty(t, user.Email, "email should default to empty")
}

La llamada require.NoError detiene el test si el parsing falla, evitando panics de puntero nil en las assertions siguientes.

¿Listo para aprobar tus entrevistas de Go?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Testing de Handlers HTTP con httptest

El paquete net/http/httptest proporciona ResponseRecorder para capturar la salida de handlers y Server para ejecutar un servidor de test local. La mayoría de los tests unitarios usan ResponseRecorder para evitar la sobrecarga de red.

handler_test.gogo
package api

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestHealthHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    rec := httptest.NewRecorder()

    HealthHandler(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `{"status": "ok"}`, rec.Body.String())
}

Para handlers que llaman servicios externos, conviene inyectar un cliente mock o usar httptest.NewServer para crear un backend ficticio.

Preguntas de Entrevista Comunes sobre Testing en Go

Los entrevistadores evalúan el conocimiento de testing a través de preguntas que revelan experiencia con bases de código reales. Estos son los patrones que distinguen a los candidatos preparados:

«¿Cómo probarías una función que llama a una API externa?»

Se debe definir una interfaz para el cliente HTTP, inyectarla en la función o struct, y proporcionar un mock que retorne respuestas predeterminadas. Esto evita llamadas de red y hace los tests determinísticos.

«¿Cuándo usarías un stub versus un mock?»

Los stubs retornan respuestas predefinidas sin verificar cómo fueron llamados. Los mocks verifican que métodos específicos fueron llamados con argumentos específicos. Los stubs son preferibles cuando el valor de retorno importa más que la interacción. Los mocks convienen cuando verificar la interacción es el objetivo del test.

«¿Qué son los tests basados en tablas y por qué Go los privilegia?»

Los tests basados en tablas definen casos de test como datos en un slice de structs. Reducen el código repetitivo, hacen trivial agregar casos y producen salida de test legible con t.Run. Este patrón se alinea con la preferencia de Go por código explícito y repetitivo sobre abstracciones complejas.

«¿Cómo manejas fixtures de test o setup compartido por múltiples tests?»

Se recomienda usar TestMain para setup y teardown a nivel de paquete, y funciones helper o suites de tests (via testify) para setup compartido entre tests relacionados. El estado global que crea acoplamiento entre tests debe evitarse.

Para preparar una entrevista de Go, el módulo de testing Go cubre estos patrones en profundidad con preguntas prácticas.

Ejecución de Tests con Cobertura y Detección de Race Conditions

El comando go test acepta flags que revelan paths de código no probados y bugs de concurrencia.

bash
# Run tests with coverage report
go test -cover ./...

# Generate HTML coverage visualization
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# Detect race conditions (slower, but catches real bugs)
go test -race ./...

El detector de race instrumenta accesos a memoria y señala lecturas y escrituras concurrentes a la misma variable. Ejecutar tests con -race en CI permite detectar bugs que solo se manifiestan bajo condiciones de timing específicas.

Organización de Tests en Bases de Código Grandes

A medida que los proyectos crecen, la organización de tests afecta la mantenibilidad:

  • Tests del mismo paquete (package foo) acceden a funciones y campos no exportados. Se usan para tests unitarios que verifican comportamiento interno.
  • Tests de paquete externo (package foo_test) importan el paquete como código externo. Se usan para tests de integración y para verificar la API pública.
  • Directorios testdata almacenan fixtures como archivos JSON, salidas de referencia o bases de datos de seed. Go los ignora durante los builds.
  • Build tags separan tests unitarios de tests de integración que requieren sistemas externos. Ejecutar go test -tags=integration ./... para incluirlos.

Para más información sobre buenas prácticas de Go más allá del testing, el artículo sobre patrones de concurrencia cubre goroutines y channels.

Puntos Clave sobre Testing en Go

  • El paquete testing viene incluido con Go y no requiere dependencias externas
  • Los tests basados en tablas reducen duplicación y son el patrón esperado en entrevistas
  • Las interfaces permiten inyección de dependencias; deben definirse en el punto de uso, no en la implementación
  • gomock genera mocks a partir de interfaces; testify proporciona assertions más claras
  • httptest.ResponseRecorder captura salida de handlers sin sobrecarga de red
  • El flag -race detecta bugs de concurrencia que los tests unitarios de otro modo no detectan
  • Las métricas de cobertura guían la atención pero no garantizan corrección

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Reto diario

¿Sabrías detectar el bug en Go?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 24 de agosto de 2026

Etiquetas

#go
#testing
#unit-tests
#mocks
#gomock
#testify

Compartir

Artículos relacionados