Saltar al contenido principal

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​

  1. Reproducir con pasos y entrada concretos.
  2. Reducir hasta conservar el ejemplo mínimo que falla.
  3. Observar resultado, test, mensaje, traza y estado relevante.
  4. Formular una hipótesis concreta: “el límite superior se acepta por error”.
  5. Comprobar con un test, un breakpoint o una observación dirigida.
  6. Corregir la causa, no solo el mensaje visible.
  7. 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:

  1. tipo de excepción: IllegalArgumentException;
  2. mensaje: Posición inexistente;
  3. frames: llamadas activas con archivo y línea;
  4. primera línea de código propio relevante;
  5. 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:

  1. mvn test reproduce expected: <2> but was: <1>;
  2. reducimos a dos tareas sin completar;
  3. colocamos un breakpoint en el for;
  4. observamos i, tareas.size() y pendientes;
  5. comprobamos que con tamaño 2, la condición es i < 1 y solo visita i == 0;
  6. corregimos la condición a i < tareas.size();
  7. 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":

  1. reproduce el fallo;
  2. formula una hipótesis;
  3. elige variables y watch útiles;
  4. indica dónde pondrías el breakpoint;
  5. 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.