# Regla: Testing Go

Go tiene soporte de testing integrado en la toolchain. Esta regla establece
cómo usarlo correctamente. El runner nativo es suficiente para la mayoría
de casos — no se necesitan frameworks externos pesados.

---

## go test como runner nativo

```bash
# Correr todos los tests del proyecto
go test ./...

# Con verbose para ver nombres de tests
go test -v ./...

# Tests de un paquete específico
go test ./internal/factura/...

# Detectar data races (OBLIGATORIO en CI)
go test -race ./...

# Con cobertura
go test -cover -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
```

---

## testify/assert para assertions legibles

La librería `testify/assert` provee mensajes de error descriptivos cuando
los tests fallan. Las funciones nativas de `testing.T` son verbosas y sus
mensajes de error poco informativos.

```go
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

// assert: el test continúa aunque falle
// require: el test se detiene si falla (para precondiciones)

func TestCrearFactura(t *testing.T) {
    // MAL — testing nativo: mensaje de error pobre
    if factura.Total != 290.00 {
        t.Errorf("expected 290.00, got %v", factura.Total)
    }

    // BIEN — testify: mensaje claro con valores esperado y actual
    assert.Equal(t, 290.00, factura.Total)
    assert.NotNil(t, factura.ID)
    assert.Equal(t, "BORRADOR", factura.Estatus)

    // require para precondiciones: si falla, no tiene sentido continuar
    require.NoError(t, err, "crear factura no debe retornar error")
    require.NotNil(t, factura, "factura no debe ser nil")
    assert.Equal(t, clienteID, factura.ClienteID)  // solo si llegamos aquí
}
```

---

## Table-driven tests obligatorios para múltiples casos

Este es el patrón idiomático de Go para tests con múltiples entradas/salidas.
Elimina la duplicación y hace los casos explícitos.

```go
func TestCalcularIva(t *testing.T) {
    // Tabla de casos de prueba
    casos := []struct {
        nombre    string
        subtotal  float64
        tasa      float64
        ivaEsper  float64
        debeError bool
    }{
        {
            nombre:   "IVA estándar 16% sobre 100",
            subtotal: 100.00, tasa: 0.16, ivaEsper: 16.00,
        },
        {
            nombre:   "IVA reducido 8% sobre 100",
            subtotal: 100.00, tasa: 0.08, ivaEsper: 8.00,
        },
        {
            nombre:   "IVA tasa cero",
            subtotal: 100.00, tasa: 0.00, ivaEsper: 0.00,
        },
        {
            nombre:     "Subtotal negativo retorna error",
            subtotal:   -10.00, tasa: 0.16, debeError: true,
        },
    }

    for _, c := range casos {
        t.Run(c.nombre, func(t *testing.T) {
            // Arrange
            calc := NuevoCalculadorIva()

            // Act
            resultado, err := calc.Calcular(c.subtotal, c.tasa)

            // Assert
            if c.debeError {
                require.Error(t, err)
                return
            }
            require.NoError(t, err)
            assert.InDelta(t, c.ivaEsper, resultado, 0.001)
        })
    }
}
```

---

## go test -race para detectar data races

Las data races son bugs no deterministas y difíciles de reproducir en producción.
Correr con `-race` en CI es obligatorio.

```bash
# En CI — falla si detecta data race
go test -race ./...
```

```go
// Ejemplo de data race que -race detecta
func TestContadorConcurrente(t *testing.T) {
    contador := &Contador{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            contador.Incrementar()  // -race detecta si Incrementar no es thread-safe
        }()
    }

    wg.Wait()
    assert.Equal(t, 1000, contador.Valor())
}
```

---

## Mocks con interfaces — sin frameworks pesados

Go no necesita frameworks de mocking pesados. Las interfaces permiten mocks simples.
Para mocks generados automáticamente: `mockery` o `moq`.

```go
// Interface del repositorio
type FacturaRepository interface {
    Guardar(ctx context.Context, f *Factura) error
    BuscarPorID(ctx context.Context, id uuid.UUID) (*Factura, error)
}

// Mock manual para tests simples
type MockFacturaRepository struct {
    GuardarFn     func(ctx context.Context, f *Factura) error
    BuscarPorIDFn func(ctx context.Context, id uuid.UUID) (*Factura, error)
}

func (m *MockFacturaRepository) Guardar(ctx context.Context, f *Factura) error {
    return m.GuardarFn(ctx, f)
}

func (m *MockFacturaRepository) BuscarPorID(ctx context.Context, id uuid.UUID) (*Factura, error) {
    return m.BuscarPorIDFn(ctx, id)
}

// Uso en test
func TestCrearFactura_GuardaYRetorna(t *testing.T) {
    facturaGuardada := &Factura{ID: uuid.New()}
    repo := &MockFacturaRepository{
        GuardarFn: func(ctx context.Context, f *Factura) error {
            return nil
        },
    }
    servicio := NuevoFacturaService(repo, zap.NewNop())

    resultado, err := servicio.Crear(context.Background(), CrearRequest{})

    require.NoError(t, err)
    assert.NotNil(t, resultado)
    assert.Equal(t, facturaGuardada.ID, resultado.ID)
}
```

---

## httptest para testing de handlers HTTP

```go
func TestHandlerCrearFactura_Exitoso(t *testing.T) {
    // Arrange
    servicio := &MockFacturaService{...}
    handler := NuevoFacturaHandler(servicio)
    body := `{"clienteId": "...","lineas": [...]}`

    req := httptest.NewRequest(http.MethodPost, "/v1/facturas",
        strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    rec := httptest.NewRecorder()

    // Act
    handler.ServeHTTP(rec, req)

    // Assert
    assert.Equal(t, http.StatusCreated, rec.Code)
    var respuesta map[string]any
    require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &respuesta))
    assert.NotNil(t, respuesta["id"])
}
```

---

## TestMain para setup/teardown global

```go
func TestMain(m *testing.M) {
    // Setup — ejecutado antes de todos los tests del paquete
    contenedor := iniciarContenedorPostgres()
    configurarDB(contenedor.ConnectionString())

    codigo := m.Run()  // ejecutar tests

    // Teardown — ejecutado después de todos los tests
    contenedor.Terminar()
    os.Exit(codigo)
}
```

---

## Cobertura: go test -cover -coverprofile

```bash
# Generar reporte de cobertura
go test -cover -coverprofile=coverage.out ./...

# Ver cobertura por función
go tool cover -func=coverage.out

# Ver umbral — falla si cobertura < 80%
COBERTURA=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
if (( $(echo "$COBERTURA < 80" | bc -l) )); then
    echo "Cobertura $COBERTURA% está por debajo del mínimo de 80%"
    exit 1
fi
```

---

## Checklist de testing Go antes de abrir PR

- [ ] `go test ./...` pasa sin errores
- [ ] `go test -race ./...` pasa sin data races
- [ ] Table-driven tests para funciones con múltiples casos
- [ ] Mocks implementados como interfaces, no con frameworks invasivos
- [ ] Cobertura del paquete >= 80%
- [ ] Handlers HTTP testeados con httptest
- [ ] Sin time.Sleep() en tests — usar canales o contextos con timeout
- [ ] Tests son deterministas (mismo resultado en cada ejecución)
