Debugging y diagnóstico
Cuando un test falla, cambiar líneas al azar puede ocultar el síntoma sin entender la causa. Diagnosticar significa convertir un fallo reproducible en evidencia y comprobar una hipótesis.
Tres tipos de fallo
- Compilación: el código no puede convertirse en clases, por ejemplo por un tipo incompatible o un símbolo inexistente.
- Runtime: el programa compila, pero durante la ejecución lanza una excepción o termina de forma inesperada.
- Lógica: compila y termina, pero produce un resultado incorrecto. Un test suele ser la señal más clara.
La categoría orienta la búsqueda, pero el proceso es el mismo.
Un proceso sistemático
- Reproducir con pasos y entrada concretos.
- Reducir hasta conservar el ejemplo mínimo que falla.
- Observar resultado, test, mensaje, traza y estado relevante.
- Formular una hipótesis concreta: “el límite superior se acepta por error”.
- Comprobar con un test, un breakpoint o una observación dirigida.
- Corregir la causa, no solo el mensaje visible.
- Volver a ejecutar los tests, incluidos los que ya pasaban.
Anota el comando exacto, por ejemplo:
mvn test -Dtest=GestorTareasTest#rechazaPosicionIgualAlTamano
Leer un stack trace
java.lang.IllegalArgumentException: Posición inexistente
at es.skilly.tareas.GestorTareas.buscar(GestorTareas.java:24)
at es.skilly.tareas.app.Main.main(Main.java:10)
Caused by: java.lang.NumberFormatException: For input string: "dos"
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at es.skilly.tareas.Entrada.leerPosicion(Entrada.java:14)
Léelo de arriba abajo y busca:
- tipo de excepción:
IllegalArgumentException; - mensaje:
Posición inexistente; - frames: llamadas activas con archivo y línea;
- primera línea de código propio relevante;
- cada
Caused by, que muestra la causa anterior envuelta por otra excepción.
No leas solo la última línea: suele ser el origen más profundo, pero necesitas el contexto de las excepciones y llamadas que la propagaron.
Debugger sin depender de un IDE
IntelliJ, Eclipse y VS Code presentan interfaces distintas, pero comparten conceptos:
- breakpoint: pausa antes de ejecutar una línea;
- resume: continúa hasta el siguiente breakpoint o el final;
- step over: ejecuta la línea sin entrar en los métodos llamados;
- step into: entra en el método invocado;
- step out: termina el método actual y vuelve al llamador;
- variables: valores visibles en el frame actual;
- watch: expresión que queremos observar durante las pausas;
- call stack: cadena de métodos que llevó al punto actual.
El debugger no adivina el fallo. Sirve para contrastar una expectativa con el estado real.
Bug de lógica guiado por un test
Este método debe calcular cuántas tareas quedan pendientes:
public int pendientes() {
int pendientes = 0;
for (int i = 0; i < tareas.size() - 1; i++) {
if (!tareas.get(i).isCompletada()) {
pendientes++;
}
}
return pendientes;
}
El código compila, pero el test detecta el error:
@Test
void cuentaTodasLasTareasPendientes() {
GestorTareas gestor = new GestorTareas();
gestor.agregar(new Tarea("Una"));
gestor.agregar(new Tarea("Dos"));
assertEquals(2, gestor.pendientes());
}
Proceso:
mvn testreproduceexpected: <2> but was: <1>;- reducimos a dos tareas sin completar;
- colocamos un breakpoint en el
for; - observamos
i,tareas.size()ypendientes; - comprobamos que con tamaño
2, la condición esi < 1y solo visitai == 0; - corregimos la condición a
i < tareas.size(); - repetimos todos los tests.
El test pasa después de la corrección y queda como protección frente al mismo off-by-one.
Fallo de runtime pequeño
public String primeraDescripcion() {
return tareas.get(0).getDescripcion();
}
Con una lista vacía aparece:
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
at java.base/jdk.internal.util.Preconditions.outOfBounds(...)
at java.base/java.util.ArrayList.get(ArrayList.java:427)
at es.skilly.tareas.GestorTareas.primeraDescripcion(GestorTareas.java:31)
La primera línea propia señala GestorTareas.java:31. La corrección depende del contrato: devolver un valor opcional o lanzar una excepción del dominio. No debemos capturar Exception y continuar como si nada, porque perderíamos la señal necesaria para diagnosticar.
Práctica
El test espera 3, pero recibe 2:
public int caracteresTotales() {
int total = 0;
for (String tarea : descripciones) {
total = tarea.length();
}
return total;
}
Con entradas "A" y "BC":
- reproduce el fallo;
- formula una hipótesis;
- elige variables y watch útiles;
- indica dónde pondrías el breakpoint;
- corrige y vuelve a probar.
Solución
La asignación sustituye el acumulado en cada vuelta. Observando total vemos 1 y después 2. La corrección es:
total += tarea.length();
El breakpoint va dentro del bucle; observamos tarea, tarea.length() y total. Tras corregir, el test recibe 3 y ejecutamos la suite completa.
Un diagnóstico útil deja tres resultados: causa explicada, corrección acotada y test que demuestra el comportamiento. En la siguiente unidad añadiremos información de ejecución mediante logging, sin convertir los logs en sustitutos del debugger o los tests.