Apuntes DAM
Volver al inicio

Caza mutantes: pruebas que detectan cada error

Ejercicio de JavaDifícilUnos 60 minutos

Escribe las pruebas de una tarifa de cine y mide su calidad con pruebas de mutación, como PIT: se ejecutan contra siete copias del código con un error pequeño cada una, y un mutante solo muere si alguna prueba lo distingue. Valores límite, clases de equivalencia y aserciones.

  • Pruebas de mutación
  • Valores límite
  • Clases de equivalencia
  • Aserciones (como en JUnit)
  • Probar excepciones
  • Calidad de una batería de pruebas

Enunciado

¿Cómo se sabe si unas pruebas son buenas? La cobertura (qué líneas se ejecutan) no basta: una prueba puede pasar por una línea sin comprobar nada útil. Las pruebas de mutación responden de otra forma: se crean copias del código con un error pequeño (un mutante: < cambiado por <=, una condición borrada…) y se ejecutan las pruebas contra cada una. Si alguna prueba falla, el mutante muere; si todas pasan, el mutante sobrevive, y eso señala un caso que nadie está comprobando.

En este ejercicio el código a probar es la tarifa de un cine (está en el código de partida): bebés de menos de 4 años gratis, niños de menos de 12 a 4 €, mayores de 65 a 5 €, estudiantes a 6 €, el resto a 8 €, los miércoles 2 € menos (salvo si es gratis) y error si la edad no está entre 0 y 120. Tu trabajo es solo el método pruebas, con las mini aserciones igual y lanza (como assertEquals y assertThrows de JUnit).

Qué tiene que hacer el programa

  1. Completa pruebas(Tarifa t) con llamadas a igual(caso, esperado, t.precio(...)) y lanza(caso, () -> t.precio(...)). No cambies nada más.
  2. Todas tus pruebas deben pasar con el código correcto (Original: todas las pruebas pasan); si una falla, el programa lo dice y se detiene.
  3. El programa ejecuta tus pruebas contra los siete mutantes y escribe, para cada uno, muerto o VIVO. El objetivo es la salida que termina en Mutantes muertos: 7 de 7. La entrada puede traer los números de los mutantes que se quieren probar, separados por espacios (en ese orden, sin repetir; lo que no sea del 1 al 7 se ignora); si no trae ninguno, se prueban todos.
  4. No hace falta probar todas las edades: con los valores límite de cada regla (justo antes y justo después de cada frontera) y un caso por cada condición sobra.

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Contra los siete mutantes

Salida por consola

Original: todas las pruebas pasan
Mutante 1 (edad < 4 cambiado por edad <= 4): muerto
Mutante 2 (edad < 12 cambiado por edad <= 12): muerto
Mutante 3 (edad >= 65 cambiado por edad > 65): muerto
Mutante 4 (se ha perdido el descuento de estudiante): muerto
Mutante 5 (el día del espectador también resta a las entradas gratis): muerto
Mutante 6 («miércoles» se compara distinguiendo mayúsculas): muerto
Mutante 7 (edad > 120 cambiado por edad >= 120): muerto
Mutantes muertos: 7 de 7

Solo los mutantes de frontera

Entrada

2 1 3 2

Salida por consola

Original: todas las pruebas pasan
Mutante 2 (edad < 12 cambiado por edad <= 12): muerto
Mutante 1 (edad < 4 cambiado por edad <= 4): muerto
Mutante 3 (edad >= 65 cambiado por edad > 65): muerto
Mutantes muertos: 3 de 3

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Lee los mutantes

Cada descripción dice qué se ha cambiado: edad < 4 cambiado por edad <= 4 solo se distingue del original con una edad de exactamente 4. Busca el caso concreto en el que cada mutante y el original dan resultados distintos.

2. Valores límite

Para cada frontera (4, 12, 65, 120) prueba el valor justo en la frontera y el anterior: 3 y 4, 11 y 12, 64 y 65, 120 y 121.

java
igual("4 años ya paga", 4, t.precio(4, false, "lunes"));
igual("3 años, gratis", 0, t.precio(3, false, "lunes"));
3. Las condiciones

Una prueba para el descuento de estudiante, otra para el miércoles en mayúsculas y otra para que un bebé siga sin pagar el miércoles.

4. Las excepciones

lanza comprueba que se lanza IllegalArgumentException. Prueba una edad imposible y, para el último mutante, que 120 sí es válida con igual.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

☕JavaCaza mutantes: pruebas que detectan cada errorDifícil

Ejemplo de lo que debe mostrar

Salida esperada
Original: todas las pruebas pasan
Mutante 1 (edad < 4 cambiado por edad <= 4): muerto
Mutante 2 (edad < 12 cambiado por edad <= 12): muerto
Mutante 3 (edad >= 65 cambiado por edad > 65): muerto
Mutante 4 (se ha perdido el descuento de estudiante): muerto
Mutante 5 (el día del espectador también resta a las entradas gratis): muerto
Mutante 6 («miércoles» se compara distinguiendo mayúsculas): muerto
Mutante 7 (edad > 120 cambiado por edad >= 120): muerto
Mutantes muertos: 7 de 7
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
java
1import java.util.ArrayList;
2import java.util.Scanner;
3import java.util.List;
4
5/** Precio de una entrada de cine (el código que hay que probar). */
6class Tarifa {
7    double precio(int edad, boolean estudiante, String dia) {
8        if (edad < 0 || edad > 120) throw new IllegalArgumentException("edad no válida: " + edad);
9        double p;
10        if (edad < 4) p = 0;
11        else if (edad < 12) p = 4;
12        else if (edad >= 65) p = 5;
13        else if (estudiante) p = 6;
14        else p = 8;
15        if (dia.equalsIgnoreCase("miércoles") && p > 0) p -= 2;   // día del espectador
16        return p;
17    }
18}
19
20// Los mutantes: copias de Tarifa con un error pequeño cada una
21class M1 extends Tarifa { double precio(int e, boolean s, String d) { if (e < 0 || e > 120) throw new IllegalArgumentException(); double p; if (e <= 4) p = 0; else if (e < 12) p = 4; else if (e >= 65) p = 5; else if (s) p = 6; else p = 8; if (d.equalsIgnoreCase("miércoles") && p > 0) p -= 2; return p; } }
22class M2 extends Tarifa { double precio(int e, boolean s, String d) { if (e < 0 || e > 120) throw new IllegalArgumentException(); double p; if (e < 4) p = 0; else if (e <= 12) p = 4; else if (e >= 65) p = 5; else if (s) p = 6; else p = 8; if (d.equalsIgnoreCase("miércoles") && p > 0) p -= 2; return p; } }
23class M3 extends Tarifa { double precio(int e, boolean s, String d) { if (e < 0 || e > 120) throw new IllegalArgumentException(); double p; if (e < 4) p = 0; else if (e < 12) p = 4; else if (e > 65) p = 5; else if (s) p = 6; else p = 8; if (d.equalsIgnoreCase("miércoles") && p > 0) p -= 2; return p; } }
24class M4 extends Tarifa { double precio(int e, boolean s, String d) { if (e < 0 || e > 120) throw new IllegalArgumentException(); double p; if (e < 4) p = 0; else if (e < 12) p = 4; else if (e >= 65) p = 5; else p = 8; if (d.equalsIgnoreCase("miércoles") && p > 0) p -= 2; return p; } }
25class M5 extends Tarifa { double precio(int e, boolean s, String d) { if (e < 0 || e > 120) throw new IllegalArgumentException(); double p; if (e < 4) p = 0; else if (e < 12) p = 4; else if (e >= 65) p = 5; else if (s) p = 6; else p = 8; if (d.equalsIgnoreCase("miércoles")) p -= 2; return p; } }
26class M6 extends Tarifa { double precio(int e, boolean s, String d) { if (e < 0 || e > 120) throw new IllegalArgumentException(); double p; if (e < 4) p = 0; else if (e < 12) p = 4; else if (e >= 65) p = 5; else if (s) p = 6; else p = 8; if (d.equals("miércoles") && p > 0) p -= 2; return p; } }
27class M7 extends Tarifa { double precio(int e, boolean s, String d) { if (e < 0 || e >= 120) throw new IllegalArgumentException(); double p; if (e < 4) p = 0; else if (e < 12) p = 4; else if (e >= 65) p = 5; else if (s) p = 6; else p = 8; if (d.equalsIgnoreCase("miércoles") && p > 0) p -= 2; return p; } }
28
29public class Main {
30    // ── Mini biblioteca de pruebas (como las aserciones de JUnit) ──
31    static List<String> fallos = new ArrayList<>();
32
33    static void igual(String caso, double esperado, double obtenido) {
34        if (Math.abs(esperado - obtenido) > 1e-9) fallos.add(caso + " (esperado " + esperado + ", obtenido " + obtenido + ")");
35    }
36
37    static void lanza(String caso, Runnable codigo) {
38        try {
39            codigo.run();
40            fallos.add(caso + " (esperaba IllegalArgumentException y no se lanzó)");
41        } catch (IllegalArgumentException e) {
42            // bien: es lo que se esperaba
43        }
44    }
45
46    /** Las pruebas: cada una, un caso con su resultado esperado. Los valores límite son los que cazan mutantes. */
47    static void pruebas(Tarifa t) {
48        igual("bebé de 3 años", 0, t.precio(3, false, "lunes"));
49        igual("4 años ya paga", 4, t.precio(4, false, "lunes"));
50        igual("11 años, infantil", 4, t.precio(11, false, "lunes"));
51        igual("12 años, adulto", 8, t.precio(12, false, "lunes"));
52        igual("64 años, adulto", 8, t.precio(64, false, "lunes"));
53        igual("65 años, jubilado", 5, t.precio(65, false, "lunes"));
54        igual("estudiante de 20", 6, t.precio(20, true, "lunes"));
55        igual("estudiante jubilado: el mejor precio", 5, t.precio(70, true, "lunes"));
56        igual("miércoles, adulto", 6, t.precio(30, false, "miércoles"));
57        igual("miércoles en mayúsculas", 6, t.precio(30, false, "MIÉRCOLES"));
58        igual("miércoles, bebé: sigue gratis", 0, t.precio(2, false, "miércoles"));
59        igual("120 años es una edad válida", 5, t.precio(120, false, "lunes"));
60        lanza("edad negativa", () -> t.precio(-1, false, "lunes"));
61        lanza("edad de 121", () -> t.precio(121, false, "lunes"));
62    }
63
64    public static void main(String[] args) {
65        pruebas(new Tarifa());
66        if (!fallos.isEmpty()) {
67            System.out.println("Original: falla " + fallos.get(0));
68            System.out.println("Una prueba que falla con el código correcto está mal escrita: corrígela antes de cazar mutantes.");
69            return;
70        }
71        System.out.println("Original: todas las pruebas pasan");
72        String[] descripciones = {
73            "edad < 4 cambiado por edad <= 4",
74            "edad < 12 cambiado por edad <= 12",
75            "edad >= 65 cambiado por edad > 65",
76            "se ha perdido el descuento de estudiante",
77            "el día del espectador también resta a las entradas gratis",
78            "«miércoles» se compara distinguiendo mayúsculas",
79            "edad > 120 cambiado por edad >= 120",
80        };
81        Tarifa[] mutantes = {new M1(), new M2(), new M3(), new M4(), new M5(), new M6(), new M7()};
82        // la entrada puede elegir qué mutantes probar (por defecto, todos)
83        List<Integer> elegidos = new ArrayList<>();
84        Scanner sc = new Scanner(System.in);
85        while (sc.hasNext()) {
86            String t = sc.next();
87            if (t.matches("[1-7]") && !elegidos.contains(Integer.parseInt(t))) elegidos.add(Integer.parseInt(t));
88        }
89        if (elegidos.isEmpty()) for (int k = 1; k <= mutantes.length; k++) elegidos.add(k);
90        int muertos = 0;
91        for (int k : elegidos) {
92            int i = k - 1;
93            fallos.clear();
94            boolean muerto;
95            try {
96                pruebas(mutantes[i]);
97                muerto = !fallos.isEmpty();
98            } catch (RuntimeException e) {
99                muerto = true;       // una excepción inesperada también delata al mutante
100            }
101            if (muerto) muertos++;
102            System.out.println("Mutante " + (i + 1) + " (" + descripciones[i] + "): " + (muerto ? "muerto" : "VIVO, falta una prueba que lo distinga"));
103        }
104        System.out.println("Mutantes muertos: " + muertos + " de " + elegidos.size());
105    }
106}

Los mutantes de frontera (< por <=) son los más comunes en el código real, y solo se cazan con valores límite: por eso el análisis de valores límite es la técnica de caja negra más rentable.

Un mutante que sobrevive no siempre es un fallo de las pruebas: a veces es equivalente (se comporta igual que el original en todos los casos). Aquí ninguno lo es, así que todos se pueden matar.

Las pruebas que solo comprueban «que no explota» matan muy pocos mutantes. Lo que mata mutantes es comprobar el resultado exacto en el caso justo.

En Java, PIT (pitest) genera los mutantes automáticamente sobre tu código y tus pruebas JUnit, y da un porcentaje de mutantes muertos que mide mejor que la cobertura la calidad de las pruebas.

Para ir más allá

  • Escribe un octavo mutante que sea equivalente al original y explica por qué ninguna prueba puede matarlo.
  • Reduce tus pruebas al mínimo número que sigue matando los siete mutantes.
  • Pasa las pruebas a JUnit 5 con @ParameterizedTest y @CsvSource.

Dónde se explica