La automatización de la prueba consiste en escribir un segmento de código para validar que otro segmento de código hace lo que debe. Dicho de otra manera, las baterías de pruebas automatizadas son software destinado a probar otro software.
En función de su alcance podemos clasificar las pruebas como:
Test unitario. Prueba una única funcionalidad aislándola de todas sus dependencias. Esta prueba suele poner el foco en una única clase.
Test de integración. Prueba que la interacción entre varios componentes funciona adecuadamente. Esta prueba suele requerir “ensamblar” una parte del producto.
Test de aceptación o GUI. Prueba todo el producto. Esta prueba requiere desplegar el producto completo.
💬 En lugar de distinguir entre unitarias, integración y prueba del sistema, Google usa el lenguaje de las pruebas pequeñas, medianas y grandes [...] El idioma real de pequeño, mediano y grande no es importante. Llámese como quiera siempre que los términos tengan un significado con el que todos estén de acuerdo. Lo importante es que los testers de Google comparten un lenguaje común para hablar sobre lo que se está probando y cómo se alcanzan esas pruebas.
James A. Whittaker, “How Google Tests Software”
Para diseñar una estrategia de testing automatizado es necesario establecer el número de pruebas de cada tipo que se elaborarán. De esta forma se indica cuántas pruebas y de qué tipo cubrirá cada segmento de código. La pirámide de pruebas de Mike Cohn, descrita en su libro “Succeeding with Agile”, ha sido un referente en este campo durante mucho tiempo. En ella Cohn establece que hay varios niveles de pruebas, y señala el grado en el que deberíamos automatizarlas. Lo ideal sería:
Muchos tests unitarios automáticos, porque un primer punto primordial para detectar fallos es a nivel de desarrollador. Si una funcionalidad en este punto falla, podrían fallar pruebas de los siguientes niveles: integración, API etc.
Bastantes tests de integración de componentes, que son los más estables y candidatos a automatizar.
Algunos tests de interfaz gráfica automatizados. Ya que estos tests son variables, lentos en su ejecución y con muchas dependencias con otros componentes.
🔎 SQLite, el popular motor de bases de datos Open Source , se compone de 155 mil líneas de código. Sus proyectos de pruebas contienen 92 millones de líneas de código. Es decir, SQLite posee 590 veces más líneas de código de pruebas que líneas de código del software propiamente dicho. How SQLite Is Tested
* Un test es frágil si se rompe con facilidad al modificar el código que pretender probar
** Un test es repetible si ante una misma entrada siempre devuelve un mismo resultado
*** En programación orientada a objetos, una pieza es una clase
**** Un elemento de infraestructura es ajenos al código, por ejemplo: ficheros, bases de datos, servicios web, procesos batch, etc.
A la hora de diseñar una estrategia de testing automatizado conviene establecer el número de pruebas de cada tipo que se elaborarán. De esta forma se indica cuántas pruebas y de qué tipo cubrirá cada segmento de código. La pirámide de pruebas de Mike Cohn, descrita en su libro “Succeeding with Agile”, ha sido un referente en este campo durante mucho tiempo. En ella Cohn establece que hay varios niveles de pruebas, y señala el grado en el que deberíamos automatizarlas. Lo ideal sería:
Muchos tests unitarios automáticos, porque un primer punto primordial para detectar fallos es a nivel de desarrollador. Si una funcionalidad en este punto falla, podrían fallar pruebas de los siguientes niveles: integración, API etc.
Bastantes tests de integración de componentes, que son los más estables y candidatos a automatizar.
Algunos tests de interfaz gráfica automatizados. Ya que estos tests son variables, lentos en su ejecución y con muchas dependencias con otros componentes.
🔍 La cobertura de código es una métrica que evalúa el porcentaje de código fuente que ha sido ejecutado por las pruebas automatizadas. Existen múltiples analizadores de código que permiten obtener la cobertura de código como NCover para C# o JaCoCo para Java.
Es importante evitar el antipatrón “pirámide invertida” o “cono de helado”. Es la visión contraria a la pirámide de Cohn: Centrar el foco en automatizar pruebas de interfaz de usuario y nada de pruebas unitarias. El cono invertido o “helado” es uno de los antipatrones más habituales. Otro de los antipatrones más extendidos es el antipatrón “cigarrillo”: ningún test unitario, ningún test de integración, ningún tests de aceptación y muchos tests manuales. Otro de los antipatrones más extendidos es el antipatrón “cigarrillo”: ningún test unitario, ningún test de integración, ningún tests de aceptación y muchos tests manuales.
Las pruebas unitarias consisten en aislar una parte del código y comprobar que funciona como se espera. Son pequeños tests que verifican el comportamiento de un objeto.
Los encargados de escribir las pruebas son los desarrolladores. Los tests unitarios ayudan a documentar el código, a refactorizarlo, a detectar regresiones y a diseñarlo:
Emerger el diseño. El programador se ve obligado a ejercitar su código. Al poner a prueba su implementación el programador sufre las consecuencias de su diseño aflorando oportunidades de mejora. Emplear un naming adecuado a la hora de definir el nombre de los test permite adquirir un mejor entendimiento del auténtico propósito que persigue el código.
Documentar el código. Los tests escritos facilitarán a otros programadores entender el propósito del código implementado. Es, además, una documentación viva, permanentemente actualizada.
Ayudan a refactorizar el código. El programador cuenta con una red de seguridad que le permite limpiar el código haciéndolo más mantenible o extensible asegurándose que los cambios realizados no han alterado el comportamiento esperado.
Permiten detectar regresiones o errores. Si el programador introduce un error, será informado con una alerta al romperse algún test.
Las pruebas unitarias tienen una estructura bien definida: Preparación, Ejecución y Validación. Para llevar a cabo buenas pruebas unitarias, deben seguir las tres A’s del Unit Testing:
Arrange (organizar, preparar). Se establecen los valores de los parámetros de entrada y se instancia el objeto o “sujeto bajo test” (SUT).
Act (actuar, ejecutar). Se desencadena el código objeto del test ejecutando un método del “sujeto bajo test”.
Assert (afirmar, validar). Se comprueba que el resultado obtenido es el esperado.
Existen abundantes frameworks (librerías) para ayudar al desarrollador a escribir las pruebas unitarias del código. Estos frameworks suelen acompañarse de plugins y extensiones para los principales IDEs de cara a facilitar tanto la codificación como la ejecución de las pruebas. Ejemplos de frameworks para testing unitario son: JUnit para java, Jasmine para javascript, Unittest para python, NUnit para C#, etc.
Los IDEs ofrecen interfaces gráficos compatibles con estos frameworks para ejecutar y consultar el resultado de los tests.
Un buen test unitario cumple las siguientes características (principio FIRST):
Fast. Rápida ejecución. Unos pocos milisegundos.
Isolated. Independiente de otros test.
Repeatable. Se puede repetir en el tiempo.
Self-Validating. Cada test debe poder validar si es correcto o no a sí mismo.
Timely. Deben escribirse en el momento adecuado durante la codificación.
En resumen, un test unitario debe ser rápido (debe ejecutarse en unos pocos milisegundos), debe ser robusto (no debe romperse al modificar el código, no es frágil) y debe ser repetible (una misma entrada debe obtener un mismo resultado en todo momento).
En los tests de integración el sujeto bajo test (SUT) es la unión (cableado o ensamblaje) de diferentes componentes del producto. Es posible “ensamblar” un componente de infraestructura de este modo el test puede acceder a ficheros de texto, a base de datos, a servicios web, etc. El uso de elementos de infraestructura “externos” a las propias clases del core de negocio puede empeorar las características del test:
Lento. Acceder al disco duro, consumir un web service o acceder a la base de datos puede llevar cientos de milisegundos.
Frágil. Al ensamblar muchos componentes, una modificación en uno de ellos puede hacer que el tests se “rompa” (deje de funcionar).
No repetible. El resultado del test (OK o NOK) puede depender del estado de los elementos de infraestructura (contenido de un fichero o columnas de un registro de base de datos)
Es habitual testar de forma automatizada el acceso a datos, la generación de informes o el consumo de una API. Todos estos tests son tests de integración.
Estos tests son lentos (consumen cientos de milisegundos) ya que acceden al disco, son frágiles porque su resultado depende de la existencia o no de ciertos ficheros y pueden ser poco repetibles porque el resultado del test depende del contenido del fichero.
El objetivo de los tests de aceptación es automatizar las comprobaciones y validaciones que los testers realizan manualmente interactuando con el interfaz gráfico del software. El caso de prueba comienza siendo manual, para luego ser automatizado.
Un ejemplo de tests de aceptación puede ser: "Dada la vista de diagnósticos tras hacer login cuando busco por código ‘aaaaaaaaa’ entonces no se devuelve se muestra un aviso indicando que ningún diagnóstico coincide con los parámetros de búsqueda".
A la hora de automatizar estas pruebas existen dos estrategias:
Usar herramientas que graban la interacción del usuario. Son habitualmente utilizadas por manager, analistas, gestores o incluso el propio cliente. Una de las herramientas “record & play” más potentes es Selenium IDE. Una vez grabado el test la herramienta es capaz de reproducir la grabación moviendo el puntero y pulsando botones y teclas. Además, es capaz de comparar la respuesta esperada con la obtenida.
Usar herramientas que permiten interaccionar con el producto software a través de comandos, lo cual permite programar las acciones del usuario. Estas herramientas son librerías y drivers que permiten escribir código para interaccionar con la GUI. Son habitualmente utilizadas por desarrolladores. Un ejemplo de estas herramientas es Selenium Web Driver. Esta herramienta permite enviar pulsaciones de teclas o clicks del ratón al navegador e inspeccionar la respuesta HTML obtenida, todo ello de forma programática.
Estas pruebas son muy lentas (pueden llevar varios segundos o incluso minutos), son muy frágiles (cambiar el nombre del botón puede romper el test) y poco ágiles (cuando un tests se pone en “rojo”, puede resultar muy difícil determinar la causa del fallo). Por otro lado, estos tests suelen requerir reescribirse con frecuencia, la interfaz de usuario es muy propensa a cambios.
🔍 El test de humo o smoke test pretende asegurar que ciertas características básicas del producto funcionan perfectamente. Por ejemplo, se implementan smoke tests para probar que al cargar las diferentes pantallas en la consola del navegador no aparecen errores o para asegurar que al pedir una pantalla de acceso restringido el usuario es redirigido a la pantalla de login. Son tests superficiales que no prueban el sistema exhaustivamente, solo pretenden probar que el producto ha desplegado todas las piezas necesarias (por ejemplo, la pantalla de login o los recursos). Las pruebas exhaustivas de cada pieza se realizan con tests unitarios y de integración.
Los smoke test son utilizados en la electrónica, concretamente en el testing de hardware: una vez se ha terminado de ensamblar la placa o equipo, se conecta a una fuente eléctrica, se enciende y si sale humo no es necesario hacer más pruebas, algo no funciona. También se utiliza este mismo término en la fontanería: para saber si existe alguna fuga en una tubería se inyecta humo y se observa si escapa por alguna fisura.
En ciertos escenarios se debe estudiar el rendimiento de la solución en función de los tiempos de respuesta de estos sistemas. Por ejemplo, durante las pruebas de carga, se estudiará la evolución del sistema en función del tamaño de los mensajes recibidos, así como del número de operaciones a realizar durante un proceso concreto. Otro tipo de prueba de rendimiento son las pruebas de estrés que someten al sistema a una carga concurrente de peticiones superior a la esperada habitualmente para encontrar el punto de corte a partir del cual el sistema se degrada.
Referencias:
• M.Cohn "Succeeding with Agile", Addison-Wesley Signature Series, 2009
• Métrica-3 Técnicas. Administración Electrónica del Gobierno de España.
• K.Beck; "Extreme Programming Explained", Addison-Wesley Professional, 1999
• R.C. Martin "Código Limpio", Anaya, 2009