Skip to content

Testing‐Strategy

Anderson Fabian Garcia Nieto edited this page Jul 9, 2026 · 2 revisions

Testing Strategy

Linker1 implementa una estrategia de testing en múltiples capas que garantiza la calidad desde el desarrollo local hasta la validación en producción. La cobertura de código se mantiene al 100% de líneas como requisito obligatorio del pipeline.


Pirámide de Testing

                    ╱╲
                   ╱  ╲
                  ╱ E2E╲         API Tests (Newman) contra producción
                 ╱──────╲        Synthetic Monitoring (Grafana)
                ╱        ╲
               ╱  Smoke   ╲     Smoke Tests en CI (app real + curl)
              ╱────────────╲
             ╱              ╲
            ╱  Integration   ╲   Tests HTTP (Javalin real + HttpClient)
           ╱──────────────────╲
          ╱                    ╲
         ╱    Unit Tests        ╲  Tests unitarios con JUnit 5 + Mockito
        ╱────────────────────────╲

Nivel 1: Tests Unitarios

Frameworks

Framework Versión Propósito
JUnit 5 (Jupiter) 5.12.2 Framework de testing
Mockito 5.18.0 Mocking de dependencias
OpenTelemetry SDK Testing (BOM) InMemoryMetricReader, InMemorySpanExporter

Clases de Test

Clase de Test Clase bajo Test Qué valida
LinkServiceTest LinkService Creación de links, deduplicación, validación de URL
LinkServiceAliasTest LinkService Aliases personalizados, conflictos, validación
LinkRepositoryTest LinkRepository Operaciones CRUD sobre SQLite in-memory
FeatureFlagsTest FeatureFlags Evaluación de flags, null safety, manejo de errores
TelemetryTest Telemetry Creación del SDK, scope de instrumentación
LinkSpansTest LinkSpans Spans, status codes, propagación de errores
SystemMetricsTest SystemMetrics Gauges de heap, threads, uptime, link count
HealthCheckTest HealthCheck Health check exitoso, fallido, spans, status
HealthCheckMetricsTest HealthCheckMetrics Contadores y histogramas de health check
MainTest Main Configuración de startup

Patrones de Testing

SQLite In-Memory

Los tests de repositorio y servicio usan bases de datos SQLite en memoria:

@BeforeAll
static void setUp() throws SQLException {
    conn = DriverManager.getConnection("jdbc:sqlite::memory:");
    try (var stmt = conn.createStatement()) {
        stmt.execute("CREATE TABLE shorturl (id TEXT PRIMARY KEY, url TEXT)");
    }
}

Esto garantiza:

  • Tests rápidos (sin I/O de disco)
  • Aislamiento total (cada suite tiene su propia base)
  • Sin cleanup necesario (la base desaparece al cerrar la conexión)

OpenTelemetry In-Memory

Los tests de telemetría usan el SDK de testing sin exportador real:

InMemoryMetricReader metricReader = InMemoryMetricReader.create();
SdkMeterProvider meterProvider = SdkMeterProvider.builder()
    .registerMetricReader(metricReader)
    .build();

// Verificar métricas después de operaciones
var metrics = metricReader.collectAllMetrics();
InMemorySpanExporter spanExporter = InMemorySpanExporter.create();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
    .build();

// Verificar spans después de operaciones
var spans = spanExporter.getFinishedSpanItems();

Inyección de Dependencias para Testing

La arquitectura DI del proyecto hace que los tests sean triviales:

// FeatureFlags sin LaunchDarkly real
var featureFlags = new FeatureFlags();  // constructor sin args → flags OFF

// StaticRoutes con resource loader fake
var routes = new StaticRoutes(featureFlags, resource -> {
    return new ByteArrayInputStream("<html>test</html>".getBytes());
});

Nivel 2: Tests de Integración HTTP

Javalin Real + HttpClient

Los tests de rutas (LinkRoutesTest, StaticRoutesTest, HealthRoutesTest) inician una instancia real de Javalin en un puerto efímero:

@BeforeAll
static void startServer() {
    app = Javalin.create();
    new LinkRoutes(service, requestMetrics, linkSpans).register(app);
    app.start(0);  // puerto aleatorio
    port = app.port();
}

Y hacen requests HTTP reales:

var request = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:" + port + "/existing"))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(301, response.statusCode());

Qué se valida

Test Escenario Verificación
LinkRoutesTest GET /{id} con link existente HTTP 301 + header Location
LinkRoutesTest GET /{id} con link inexistente HTTP 404
LinkRoutesTest POST /link con URL válida HTTP 201 + crea link
LinkRoutesTest POST /link con URL inválida HTTP 400
LinkRoutesTest POST /link con alias HTTP 201 + alias personalizado
LinkRoutesTest POST /link con alias duplicado HTTP 409 (Conflict)
LinkRoutesTest POST /link JSON body HTTP 201 (JSON parsing)
LinkRoutesErrorHandlingTest Fallo interno del servicio HTTP 500 + métricas de error
StaticRoutesTest GET / con flag OFF Sirve index.html (V1)
StaticRoutesTest GET / con flag ON Sirve index-v2.html (V2)
StaticRoutesTest GET /styles.css con flag toggle Sirve CSS correcto
StaticRoutesTest Recurso no encontrado HTTP 404
HealthRoutesTest MySQL alcanzable HTTP 200 + body "OK"
HealthRoutesTest MySQL inalcanzable HTTP 503 + body "Unhealthy: ..."

Nivel 3: Smoke Tests (CI)

En el job Smoke Test de ci.yml, la aplicación se ejecuta desde el fat JAR real:

- name: Start app
  env:
    LD_SDK_KEY: ${{ secrets.LD_SDK_KEY }}
  run: |
    chmod +x linker1
    LINKER_PORT=8080 ./linker1 &
    # Esperar hasta 20 segundos por el startup
    for i in $(seq 1 20); do
      curl -sf http://localhost:8080/ > /dev/null && break
      sleep 1
    done

Validaciones

Test Comando Esperado
Página principal curl -sf localhost:8080/ HTTP 200
JavaScript curl -sf localhost:8080/app.js HTTP 200
CSS curl -sf localhost:8080/styles.css HTTP 200
Crear link POST /link {"url":"https://example.com"} ID retornado
Redirect GET /{id} HTTP 301
Alias POST /link {"url":"...","alias":"ci-smoke-alias"} HTTP 201
Conflicto POST /link {"alias":"ci-smoke-alias"} (duplicado) HTTP 409
Not found GET /does-not-exist HTTP 404

Diferencia con Unit Tests

Los smoke tests validan lo que los unit tests no pueden:

Aspecto Unit Tests Smoke Tests
Artefacto Clases individuales Fat JAR empaquetado
Dependencias In-memory / mocked Reales (SQLite, LaunchDarkly)
SPI/classpath Maven classpath Assembly classpath (fat JAR)
Network stack HttpClient → Javalin directo curl → Jetty completo

Nivel 4: API Tests (Newman)

La colección de Postman (postman/linker1.postman_collection.json) se ejecuta con Newman contra la instancia real de producción:

- name: Run Newman against the live instance
  run: |
    npx --yes newman run postman/linker1.postman_collection.json \
      --env-var "baseUrl=https://1.n-la-c.app" \
      --reporters cli,junit --reporter-junit-export newman-report.xml

Esto valida:

  • Que la API de producción responde correctamente
  • Que el DNS y HTTPS funcionan
  • Que Nginx proxea correctamente a Javalin
  • Que el comportamiento end-to-end coincide con lo esperado

¿Por qué contra producción y no staging?

No existe un entorno de staging separado (solo hay una VM). Los API tests de CI solo corren en push (no en PRs), para no ejecutarse innecesariamente en cada PR. Son read-only en su mayoría para no contaminar la base de datos de producción.


Nivel 5: Validación Post-Deploy

El job validate-prod en pipeline.yml ejecuta un smoke test de solo lectura contra producción después de cada deploy:

- name: Smoke test production (read-only)
  run: |
    curl -sf "https://1.n-la-c.app/" > /dev/null
    curl -sf "https://1.n-la-c.app/app.js" > /dev/null
    curl -sf "https://1.n-la-c.app/styles.css" > /dev/null
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://1.n-la-c.app/does-not-exist")
    test "$STATUS" = "404"

Si esta validación falla, se activa el rollback automático.


Nivel 6: Synthetic Monitoring

Grafana Cloud ejecuta un HTTP check contra https://1.n-la-c.app/healthz cada 5 minutos, 24/7. Esto funciona como un test continuo que:

  • Verifica la disponibilidad del servicio
  • Mide la latencia
  • Genera métricas de uptime
  • Alerta si el servicio no responde

Cobertura de Código

Configuración de JaCoCo

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <id>check</id>
            <phase>verify</phase>
            <goals><goal>check</goal></goals>
            <configuration>
                <excludes>
                    <exclude>Main.class</exclude>
                </excludes>
                <rules>
                    <rule>
                        <element>BUNDLE</element>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>1.00</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

Reglas

Aspecto Configuración
Umbral 100% de líneas
Scope Bundle completo
Exclusiones Solo Main.class (composition root, difícil de testear unitariamente)
Fase verify (se ejecuta después de los tests)
En CI mvn -B -ntp verify -DskipTests valida la cobertura

¿Por qué 100%?

  • Fuerza al equipo a escribir tests para cada línea de código
  • Impide que código sin tests pase a producción
  • Main.class se excluye porque es el composition root (wiring, no lógica)
  • El reporte de cobertura se sube como artefacto de CI para revisión

Mapa de Test Files

test/
├── MainTest.java                          ← Tests de configuración de startup
└── linker/
    ├── LinkRepositoryTest.java            ← CRUD sobre SQLite in-memory
    ├── LinkServiceTest.java               ← Lógica de negocio (crear, deduplicar)
    ├── LinkServiceAliasTest.java          ← Aliases: crear, conflictos, validación
    ├── config/
    │   └── FeatureFlagsTest.java          ← Evaluación de flags, null safety
    ├── health/
    │   ├── HealthCheckTest.java           ← Health check: OK, fail, spans
    │   ├── HealthCheckMetricsTest.java    ← Contadores y histogramas
    │   └── HealthRoutesTest.java          ← HTTP 200/503 end-to-end
    ├── routes/
    │   ├── LinkRoutesTest.java            ← Endpoints HTTP completos
    │   ├── LinkRoutesErrorHandlingTest.java ← Manejo de errores 500
    │   └── StaticRoutesTest.java          ← Servir HTML/CSS con feature flags
    └── telemetry/
        ├── TelemetryTest.java             ← Inicialización del SDK
        ├── LinkSpansTest.java             ← Traces: create, resolve, errores
        └── SystemMetricsTest.java         ← Gauges: heap, threads, uptime

postman/
└── linker1.postman_collection.json        ← API tests (Newman)

Total: 16 archivos de test cubriendo todos los paquetes del proyecto.


Resumen de la Estrategia

Nivel Herramienta Cuándo corre Qué valida
Unit JUnit 5 + Mockito Cada build Lógica de negocio, telemetría, flags
Integration Javalin + HttpClient Cada build Endpoints HTTP, rutas, respuestas
Smoke curl + fat JAR Cada build CI Artefacto empaquetado, startup, funcionalidad
API E2E Newman (Postman) Push a main Producción end-to-end
Post-deploy curl + producción Cada deploy Deployment exitoso
Synthetic Grafana Cloud Cada 5 min 24/7 Disponibilidad continua
Coverage JaCoCo Cada build 100% de líneas

Clone this wiki locally