Pruebas Continuas

Publicado en

Uno de los principales elementos del desarrollo ágil es la entrega continua y regular de valor. Por ello han surgido técnicas como la integración continua, la cual nos ayuda a tener un software compilable en todo momento. Pero no basta con que el software únicamente se pueda compilar, de hecho es el primer paso de toda una serie de elementos y características que debe cumplir el software.

Una práctica que ayuda a mitigar el nivel de riesgo introducido por los cambios creados con las modificaciones realizadas durante un sprint de desarrollo, es el ejecutar de manera constante, selectiva o completa las pruebas de software automatizadas que se tengan para las diferentes fases del ciclo de vida del software. Así pues, las pruebas unitarias serán las que más frecuentemente y de manera completa se ejecuten y por otro lado, las pruebas encargadas de ejercitar el software para medir su desempeño serán las que menos se ejecuten.

Adaptándonos

Lo mencionado anteriormente conlleva un esfuerzo extra, ya que adicional a las labores de diseño y codificación, el equipo debe encontrar tiempo para ejecutar estas pruebas. Por ello la solución lógica es emplear el mismo modelo de continuidad empleado en la construcción del software, pero aplicado a las pruebas. Esta técnica se llama Pruebas Continuas (continuous testing) y  consiste en adaptar a nuestro ciclo de vida fases donde se ejecuten las pruebas de software de los diferentes niveles de manera regular, con el fin de tener una retroalimentación acelerada para con nuestro equipo de  desarrollo. De esta manera podemos ser más preventivos que reactivos.

Esto no es una labor sencilla, ya que involucra un cambio en el proceso. Primeramente es necesario tener una responsabilidad hacia todos los miembros del equipo sobre codificar y mantener las pruebas de software, También hay que seleccionar o construir las herramientas necesarias y adoptarlas. Finalmente hay que construir los mecanismos de visibilidad, es decir, la forma en la cual todos los miembros del equipo conocen y emplean la información generada de manera oportuna.

¿Por dónde iniciamos?

La mayoría de los desarrolladores y testers en nuestra región no están familiarizados con técnicas formales de diseño de pruebas y mucho menos con patrones de diseño de pruebas, por lo que es indispensable crear conciencia en el equipo de esta área de oportunidad para poder aprovecharla.

Para comenzar, lo primero que debe realizar el equipo es construir una prueba por cada pieza  de software que se implemente. La prueba debe verificar que el software “hace lo que tiene que hacer”, es decir, deberá revisar que en los casos “normales” del flujo de operación la pieza de software devuelve un resultado acorde al diseño, esto es lo que conocemos comúnmente como una prueba unitaria automatizada.

Principios y lineamientos

Estos son algunos principios que recomiendo tener en cuenta para el diseño de pruebas:

  • Las pruebas de software deben reducir riesgo, no introducirlo.
  • La ejecución de las pruebas de software debe ser sencilla.
  • Las pruebas de software deben ser fáciles de mantener.

Adicional podemos ejecutar prácticas comunes del buen diseño de pruebas como por ejemplo:

  • Cada prueba unitaria debe fallar solo por una razón. Es decir, cada prueba solo revisa una cosa a la vez. Es una muy mala práctica tener más de una verificación en una sola prueba.
  • Sólo debe existir una prueba para una razón de fallo. Es así que no debe haber dos o más pruebas que fallen por el mismo motivo. Lo anterior ayuda a mantener una línea base de pruebas claramente definidas.
  • Las pruebas deben ser completamente independientes de base de datos, de otros tests, de archivos, ui, etc.

Bajo estas premisas es muy sencillo realizar un proyecto de pruebas de software y que el mismo sea llevado a buen fin.

Organización

Una vez construidas nuestras pruebas de software es necesario agruparlas, con el objetivo de tener una clara separación de qué partes son las que afectamos. Por ejemplo, podríamos separarlas por funcionalidad del producto, por capas de la arquitectura o por cualquier otro elemento. El único requisito es que sea fácilmente trazable cualquier prueba, es decir que sepamos donde está el área de influencia de la misma. Con las pruebas ya separadas formamos “suites”, cuyo objetivo es que puedan ser otro elemento de organización del proyecto, así al tener las pruebas segmentadas podemos ejecutarlas de manera selectiva.

Lo siguiente a contemplar es la selección y uso de un TestRunner, una herramienta que se encargará de ejecutar los tests y presentar los resultados. Esta herramienta deberá tener una interfaz por línea de comando debido a que será la pieza que servirá para comunicarse con el servidor de integración continua.

Ya con todas las herramientas necesarias, es preciso integrarlas de modo que no afecten nuestro ciclo de desarrollo. Básicamente se trata de introducir una serie de pasos más a nuestro actual flujo de trabajo, siempre de manera ordenada y coherente.

¿Gasto o inversión?

Es bien sabido que este tipo de pruebas requieren un esfuerzo adicional y requieren un alto grado de conocimiento de la herramienta que se empleará, pero es una inversión que muestra su valía una vez que los proyectos van creciendo y se vuelven más complejos. El papel de estas pruebas consiste en ejercitar todas las capas de la aplicación así como verificar que las piezas de software se comunican de manera correcta.

Uno de los elementos clave de la práctica es el definir el momento y la frecuencia con la cual ejecutaremos los distintos tipos de prueba. Para ello es de suma importancia considerar cómo afectaremos nuestro proceso de entrega. Primeramente todas las pruebas unitarias deben agregarse al proceso de compilación e integración continua, así con cada cambio realizado en nuestro sistema de gestión de la configuración, el servidor de integración continua deberá obtener la última versión del código del proyecto y de manera conjunta realizar la compilación de los componentes y ejecutar las pruebas unitarias. Es importante recalcar que deberá ser un servidor de integración continua quien ejecute esta acción y no los desarrolladores, con el fin de no afectar los tiempos de construcción ni consumir recursos innecesarios.

La ejecución de estas pruebas será el mecanismo con retroalimentación más temprana dentro de nuestro proceso de construcción, por lo que es muy importante cuidar elementos como la velocidad de ejecución y robustez de las pruebas.

¿Y los demás tipos de prueba?

Las pruebas unitarias no son el único tipo de prueba que se puede ejecutar dentro de un flujo continuo. Si bien es cierto que son las más baratas y efectivas, no son las únicas que deben ser tomadas en cuenta en un contexto completo. Por ejemplo, es sumamente complicado realizar una prueba de integración exclusivamente con tests unitarios ya que por su naturaleza son independientes, es en estas situaciones donde hay que considerar otras estrategias basadas en pruebas de caja negra.

Los otros tipos de prueba (funcionales, aceptación y desempeño) típicamente son costosas en tiempo o recursos de cómputo, por lo que no es recomendable integrarlas en su totalidad a nuestro flujo continuo de prueba.

Una buena técnica es hacer suites de pruebas dinámicas, es decir conjuntos de pruebas que se adapten a los cambios mediante un elemento de control. Por ejemplo, se puede implementar un mapa de la funcionalidad asociada a módulos y piezas de software en particular así como sus áreas directas de influencia. De esta forma es fácilmente rastrear qué tests ejecutar.

Asociando esto a un mecanismo de mensajes mediante la descripción del commit podemos indicar qué funcionalidad se afecta, de tal modo que al obtener los últimos cambios en nuestro código también obtenemos la información para armar dicha suite. Un ejemplo de mensaje sería el siguiente:

Número_de_issue  Tipo_de_cambio  Módulo_funcional prioridad

En el cual:

Número_de_issue: es el identificador de nuestro sistema de issues, por ejemplo el número de bug, el número de requerimiento, etcétera.

Tipo_de_cambio: indica de qué tipo es el código que estamos introduciendo, por ejemplo si es nueva funcionalidad, la solución a un bug, un cambio debido a una refactorización.

Módulo_funcional: es el identificador de alto nivel que nos dice la funcionalidad a la que afecta el cambio.

Prioridad: es una escala numérica que indica la relación entre nivel de riesgo del componente respecto a la frecuencia de uso.

Con lo anterior podemos reunir la información respecto a los tests a ejecutar, así como orden de ejecución de los mismos.

Adicional a lo antes mencionado es recomendable tener un mecanismo para ejecutar la totalidad de las pruebas. Aquí debemos ser muy cuidadosos con la frecuencia de ejecución, nuevamente debido al alto costo. Una buena práctica es ejecutarlas una vez a la semana, pudiendo ser los fines de semana ya que así al inicio de semana se tendría información más completa del estado real de la totalidad del proyecto.

La ejecución continua de las pruebas de desempeño es un asunto bastante debatible debido a que este tipo de prueba es aplicable generalmente a proyectos de software bastante maduros, por lo que su ejecución se debe programar acorde a la situación del proyecto.

Conclusión

La práctica de pruebas continuas enriquece y agrega valor a la integración continua. En este artículo he mostrado algunos aspectos clave. Te invito a conocer más sobre este fascinante tema.

 

Bio

Christian Ramírez (@chrix2) es Ingeniero en computación por la UNAM y astrónomo aficionado. Actualmente es consultor de testing y dedica la mayor parte de su tiempo a ayudar a mejorar la práctica de testing en los proyectos donde participa. Es ponente regular en eventos internacionales sobre Software Testing y Agile. Es amante de python y ferviente creyente de que el conocimiento debe ser libre.