Skip to content

Guía de Pruebas Unitarias

Go-MexPost sigue una estrategia TDD (Test-Driven Development) con pruebas en tres capas: servicios, algoritmo geoespacial y handlers HTTP. Todas las pruebas usan exclusivamente mocks (sin tocar la base de datos real), lo que las hace deterministas, rápidas y desacopladas.


Estructura de los paquetes de prueba

internal/
├── core/services/
│   ├── colonia_srv_test.go   ← Pruebas de lógica de negocio (con MockRepo)
│   └── geometry_test.go      ← Pruebas del algoritmo Ray-Casting
└── adapters/handler/
    └── http_handler_test.go  ← Pruebas de endpoints HTTP (con MockService)

Ejecutar las pruebas

bash
# Todos los paquetes
go test ./...

# Un paquete específico con salida verbosa
go test -v ./internal/core/services/...
go test -v ./internal/adapters/handler/...

# Con reporte de cobertura
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out

Capa 1 — Servicios (colonia_srv_test.go)

Las pruebas de servicio verifican todas las reglas de negocio y los flujos de éxito/error sin depender de SQLite. Se usa un MockRepo que implementa la interfaz ports.ColoniaRepository.

MockRepo

go
type MockRepo struct {
    Err        error                    // error a simular
    Colonias   []domain.Colonia         // datos a devolver
    Municipios []domain.Municipio

    LastColoniaFilter ports.ColoniaSearchFilter   // captura el último filtro recibido
    LastGeoFilter     ports.ReverseGeocodeFilter
}

func (m *MockRepo) SearchColonias(filter ports.ColoniaSearchFilter) ([]domain.Colonia, error) { ... }
func (m *MockRepo) CountColonias(filter ports.ColoniaSearchFilter) (int, error) {
    if m.Err != nil { return 0, m.Err }
    return len(m.Colonias), nil
}

El campo Last*Filter permite verificar que el servicio construyó y propagó correctamente el filtro hacia el repositorio.

Pruebas incluidas

TestVerifica
TestBuscarColonias_ErroresDeValidacionValidationError cuando falta cp y nombre
TestBuscarColonias_CPInvalidoPorLongitudValidationError cuando cp tiene menos de 3 dígitos
TestBuscarColonias_CPInvalidoConLetrasValidationError cuando cp contiene letras
TestBuscarColonias_ExitoSinGeoResultado correcto y que Geometria=nil cuando incluirGeo=false
TestBuscarColonias_RepoErrorPropagación de errores del repositorio
TestBuscarMunicipios_NotFoundErrNotFound cuando el repo devuelve lista vacía
TestBuscarPorCoordenadas_ValidaRangosValidationError cuando lat > 90
TestBuscarPorCoordenadas_PointInPolygonPunto dentro del polígono → colonia encontrada
TestBuscarPorCoordenadas_NotFoundPunto fuera del polígono → ErrNotFound

Ejemplo detallado: prueba de omisión de geometría

go
func TestBuscarColonias_ExitoSinGeo(t *testing.T) {
    geo := `{"type":"Polygon","coordinates":[...]}`
    mockDatos := []domain.Colonia{{Codigo: "06700", Nombre: "Roma Norte", Geometria: &geo}}
    repo := &MockRepo{Colonias: mockDatos}
    servicio := services.NewColoniaService(repo)

    // Pedimos sin geometría (incluirGeo=false)
    resultado, err := servicio.BuscarColonias(ports.ColoniaSearchFilter{CP: "067"}, false)

    assert.NoError(t, err)
    assert.Len(t, resultado, 1)
    assert.Equal(t, "Roma Norte", resultado[0].Nombre)
    assert.Nil(t, resultado[0].Geometria)                   // ← geometría eliminada por el servicio
    assert.Equal(t, "067", repo.LastColoniaFilter.CP)       // ← filtro propagado correctamente
}

Capa 2 — Algoritmo Geoespacial (geometry_test.go)

Prueba directamente PointInGeoJSON con polígonos simulados para validar que el Ray-Casting funciona correctamente en los tres casos clave.

Polígono de prueba

Se usa un cuadrado unitario simple como geometría de referencia:

json
{
  "type": "Polygon",
  "coordinates": [[[0,0],[10,0],[10,10],[0,10],[0,0]]]
}
    (0,10)─────(10,10)
       │           │
       │    ●(5,5) │   ← DENTRO
       │           │
    (0,0)──────(10,0)

●(15,5)               ← FUERA

●(0,5)                ← BORDE (se trata como DENTRO)

Pruebas incluidas

TestPuntoResultado esperado
TestPointInGeoJSON_InsideSquare(5, 5) — centro del cuadradotrue
TestPointInGeoJSON_OutsideSquare(15, 5) — a la derechafalse
TestPointInGeoJSON_BoundarySquare(0, 5) — sobre el borde izquierdotrue

Ejemplo

go
func TestPointInGeoJSON_InsideSquare(t *testing.T) {
    geo := `{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}`

    inside, err := services.PointInGeoJSON(5, 5, geo)

    assert.NoError(t, err)
    assert.True(t, inside)
}

Nota: PointInGeoJSON(lon, lat, ...). El primer argumento es longitud (X), el segundo latitud (Y), siguiendo la convención GeoJSON.


Capa 3 — Handlers HTTP (http_handler_test.go)

Prueba los endpoints Gin usando net/http/httptest para simular peticiones reales sin levantar un servidor. Se usa un MockService que implementa ports.ColoniaService.

MockService

go
type MockService struct {
    Err       error
    Colonias  []domain.Colonia
    Municipio []domain.Municipio
}

func (m *MockService) BuscarColonias(...) ([]domain.Colonia, error) { ... }
func (m *MockService) ContarColonias(filter ports.ColoniaSearchFilter) (int, error) {
    if m.Err != nil { return 0, m.Err }
    return len(m.Colonias), nil
}

Patrón de prueba

go
gin.SetMode(gin.TestMode)               // 1. Modo de prueba (sin logging)
router := gin.New()                     // 2. Router limpio (sin middlewares)
router.GET("/colonias", manejador.BuscarColonias)

req, _ := http.NewRequest("GET", "/colonias?cp=067", nil)
w := httptest.NewRecorder()             // 3. Captura la respuesta en memoria
router.ServeHTTP(w, req)               // 4. Ejecuta la petición

assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), `"codigo":"06700"`)

Pruebas incluidas

TestEndpointEscenarioCódigo esperado
TestBuscarColonias_Status400GET /coloniasMockService devuelve ValidationError400
TestBuscarColonias_Status400CPInvalidoGET /colonias?cp=14CP muy corto400, "cp invalido"
TestBuscarColonias_Status400LimitInvalidoGET /colonias?cp=067&limit=abclimit no numérico400, "limit debe ser..."
TestBuscarColonias_Status200SinGeoGET /colonias?cp=067Éxito, sin incluir_geo200, pagina, total_paginas presentes
TestBuscarColonias_PaginacionMetadataGET /colonias?cp=067&limit=2&pagina=15 resultados, página 1 de 3200, pagina_siguiente presente, pagina_anterior null
TestBuscarColonias_Status500GET /colonias?cp=067MockService devuelve error genérico500
TestBuscarMunicipios_Status404GET /municipios?estado_id=09MockService devuelve ErrNotFound404, "ajusta tus filtros"
TestBuscarCoordenadas_Status400PorParametrosGET /coordenadas?lat=19.4Falta el parámetro lon400
TestBuscarCoordenadas_Status200ConGeoGET /coordenadas?lat=19.4&lon=-99.1&incluir_geo=trueÉxito con geometría200, con campos geometria y centro_lon