-
Notifications
You must be signed in to change notification settings - Fork 1
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.
╱╲
╱ ╲
╱ 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
╱────────────────────────╲
| 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
|
| 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 |
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)
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();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());
});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());| 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: ..." |
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| 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 |
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 |
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.xmlEsto 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
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.
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.
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
<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>| 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 |
- Fuerza al equipo a escribir tests para cada línea de código
- Impide que código sin tests pase a producción
-
Main.classse excluye porque es el composition root (wiring, no lógica) - El reporte de cobertura se sube como artefacto de CI para revisión
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.
| 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 |