¿Por qué hay que probar el código de algún modo?
Las pruebas unitarias son útiles porque nos ayudan a crear casos para verificar que el software se comporta como estamos esperando. Tal vez pienses que programas muy bien, pero la realidad es que los bugs se nos pueden escapar incluso por muy seguro que estemos, a nada que nos traicionen las manos mientras estamos tecleando código. Es por ello que es necesario verificar que el software funciona como piden los requisitos. Las pruebas de software son una manera de comprobar que el software que hemos escrito no tiene bugs aparentes. Dicho sea de paso, para el código del que vayamos a hacer un test, podremos deducir que la parte que estamos probando no tiene bugs que sean visibles en ese momento. Supongamos que tenemos un código como el siguiente:
// record Producto(int precio) { }
int total(List<Producto> productos) {
int sum = 0;
for (Producto p : productos) {
sum = p.precio();
}
return sum;
}
Como mínimo, sería de desear que en el momento de escribir este código tirase algún tipo de ejemplo para comprobar si lo he programado bien. En el peor de los casos, podría escribir un pequeño Main:
public static void main(String args[]) {
Producto p1 = new Producto(400);
Producto p2 = new Producto(200);
List<Producto> ps = Arrays.asList(p1, p2);
int sum = total(ps);
System.out.println(sum);
}
Si hiciese eso, y examinase con cuidado la salida del programa comparándolo con lo que debería obtener después de leer el código, podría darme cuenta que aquí hay un bug, en tanto que la salida de este programa será 200, mientras que realmente sabemos que el precio total de un carro con dos productos de 400 y 200 dólares debería marcar 600. Algo ha salido mal. Si regresamos al código vemos que la función total tiene un bug porque dentro del bucle for estoy usando el operador = en vez del operador +=. Por muy bien que pueda pensar que programe, es necesario admitir que se me puede ir la mano con el teclado en cualquier momento. Después de corregir el bug,
int total(List<Producto> productos) {
int sum = 0;
for (Producto p : productos) {
sum += p.precio();
}
return sum;
}
Ejecutar el main pasa a imprimir 600.
¿Cuál es el problema con este tipo de pruebas?
Probar que el software funciona mediante métodos main no tiene nada de malo. Es una técnica de testing como cualquier otra. Se ejecuta un programa pensado exclusivamente para analizar el funcionamiento del software y se verifica la salida con respecto a una plantilla de resultados que de antemano han sido verificados que deben darse. Por ejemplo, en este caso cada vez que ejecutamos el main tenemos que comparar la salida recibida con el valor 600.
En algunos entornos, es posible que este sea el único enfoque posible. Por ejemplo, supongamos que se trata de un programa que interactúa con un mundo en 3D, como puede ser un videojuego o un simulador que requiera presentar gráficos complejos por pantalla. Es posible que sea necesario contar con la ayuda de una persona que verifique manualmente que al ejecutar un programa especial se obtiene una salida visual similar a la de una imagen de referencia. Por lo menos, esto será necesario hasta que las inteligencias artificiales avancen lo suficiente como para poder automatizar esto.aasdasd
Sin embargo, trabajar con este paradigma requiere que cada vez que se hagan cambios al programa se verifiquen todos los casos de prueba que hayamos desarrollado de forma manual. Eso significa que en programas grandes, tendríamos que ejecutar manualmente docenas o cientos de funciones para verificar que no ha cambiado la salida que obtenemos. En el caso de que el software esté bien diseñado, este número se podría reducir. Sin embargo, cuando se introducen conceptos como el acoplamiento en una base de código, hay que considerar que un cambio en un módulo puede provocar cambios inesperados en otros módulos que interactúen con él. Es por ello que a veces lo que puede parecer un cambio inadvertido en una función de apoyo puede acabar introduciendo errores en las partes menos inesperadas del código.
Sin embargo, estar todo el día probando todas las funciones del código no es útil ni eficiente, salvo que sea expresamente tu trabajo. Aquí, automatizar el proceso de pruebas donde sea posible es algo que puede ayudar a que el equipo de desarrollo tenga más agilidad y confianza para comprobar que el código que está tecleando es el correcto, o por lo menos que no falla de las maneras más evidentes.
¿Qué es una prueba unitaria?
Una prueba unitaria va a consistir en un archivo de código fuente con funciones que permite llevar a cabo una determinada aserción respecto a nuestro código. Algunos ejemplos de lo que las pruebas nos permiten comprobar:
- ¿El resultado de llamar a un método con un determinado parámetro devuelve una salida concreta?
- ¿Se lanza una excepción en este método cuando se le pasa un parámetro concreto o nulo?
- ¿El array que devuelve este método tiene la estructura esperada?
Cada una de estas comprobaciones se denominará aserciones o asertos. En una prueba manual sería lo que hacemos cada vez que comprobamos si una línea de texto escrita mediante System.out.println se corresponde con lo que debería dar en función de lo que diga nuestra plantilla. Cuando lo automatizamos, utilizamos un framework de testing, que nos proporciona funciones que se ocupan de hacer estas comprobaciones por nosotros y marcar un error en caso de que la llamada no sea satisfactoria.
Un programa de ordenador grande puede tener cientos de pruebas unitarias con miles de asertos para cubrir el código fuente de una manera que nos permita asegurar que el código se está comportando en todo momento como debe comportarse. La ventaja de este sistema es que podemos pedirle al ordenador docenas de veces al día que nos ejecute todos los mil asertos uno tras otro y jamás habrá una queja. En cambio, un humano probablemente no podría hacer el mismo trabajo sin cansarse y dejar de prestar tanta atención a lo que está haciendo. De este modo, podemos invertir recursos en cosas más prioritarias e importantes que en una suite de tests que queda completamente automatizada y que puede ser lanzada por cada commit o incluso cada vez que guardamos un archivo, pudiendo cubrir en cada lanzamiento miles de líneas de código de test.
En el caso del curso que estás siguiendo, nos vamos a centrar en JUnit 5, un framework de pruebas para el lenguaje de programación Java, con el que es posible verificar que nuestro código Java se comporta como cabe esperar mediante un ámplio catálogo de asertos que tenemos a nuestra disposición para usar.