archivo

Archivo de la etiqueta: pruebas unitarias

Mutation testing es una técnica de pruebas de caja blanca que surgió en la década de los setenta que se utiliza para realizar una verificación de la calidad de los métodos y procedimientos de testing utilizados.

El motivo principal de la utilización de este tipo de estrategias es la de comprobar si las pruebas definidas son correctas y cubren los requerimientos del sistema que se está verificando. A veces se definen pruebas que efectivamente dan por válidas las salidas correctas de un determinado programa, pero que también dan por válidas otras salidas que deberían ser erróneas, es decir, nos centramos en comprobar que el resultados que esperamos se produce sin pensar en que lo mismo hay otras formas anómalas de producir resultados que además son incorrectos.

¿En qué consiste la técnica? En realizar ligeras modificaciones al código fuente original que deberían provocar unos resultados diferentes al normal y comprobar si la técnica de testing utilizada puede detectar estos cambios. Ejemplos de estos pequeños cambios lo podemos tener modificando el operador en las guardas de sentencias selectivas o iterativas, eliminando sentencias, etc…

Si la técnica de testing no ha detectado el cambio puede ser por dos motivos, por un lado que los cambios en el código fuente no han sido ejecutados (ya sea porque ese fragmento de código no se ejecuta nunca) o porque la estrategia de testing ha fallado. Para intentar que este tipo de situaciones no provoque falsos positivos lo que se hace es incluir mutaciones en zonas cubiertas por pruebas unitarias (y que se ha verificado que el código puede llegar a ejecutarse) o bien realizar mutaciones en diversas zonas del código.

La rigurosidad del testing debe depender del nivel de criticidad del sistema que se va a poner en producción, ya sea en primera instancia o como consecuencia de una evolución del mismo. Esta rigurosidad se tendrá que ver reflejada en todas las etapas de desarrollo del software y tiene como objetivo evitar que lleguen a producción errores que sean críticos para el sistema y que puedan provocar un coste en vidas humanas o unas pérdidas económicas que puedan poner en jaque la subsistencia de una organización.

Esta rigurosidad estará relacionada con el número de casos de prueba a los que se somete el software, desde la misma definición de las pruebas unitarias a las pruebas de carácter funcional, así como a la propia verificación de la bondad de la técnica utilizada.

Hoy en día existen implantaciones software en sistemas empotrados, sistemas de información, etc… que manejan todo tipo de cosas. Como cada vez está más extendida la automatización, cada vez son mayores los sectores donde el software maneja funciones críticas. Incluso funciones que se pueden considerar no críticas, como por ejemplo la web de una organización, cada vez tiene un mayor grado de criticidad tanto en cuanto la forma en que muchos clientes acceden a la misma es través de Internet.

Por tanto, cada día que pasa es más importante que un producto llegue a producción tras superar un umbral de calidad específico definido para el mismo que permita tener confianza en una reducción importante de la probabilidad de que se produzcan errores críticos.

Hacer un buen testing no es sencillo, así como seleccionar el conjunto finito de casos que se va a estudiar (incrementar la cardinalidad de ese conjunto depende de factores económicos y el presupuesto debería estar ajustado a la criticidad del producto), ya que de el mismo depende que posibles errores importantes se puedan colar en producción.

Sobre esto es conveniente recordar una cita muy conocida de Edsger Wybe Dijkstra en la que comenta que: “El testing de los programas puede ser muy efectivo para mostrar la presencia de errores, sin embargo resulta inadecuado para mostrar su ausencia”.

Uno de los aspectos más complicados del testing consiste en definir hasta dónde se va a llegar, es decir, qué tipo de pruebas realizar y con qué intensidad, ya que todo el software no tiene la misma criticidad, los mismos recursos para su desarrollo, los mismos plazos, etc…

Es un error aplicar el mismo nivel de testing a todos los productos, ya que podemos pecar por exceso o por defecto (siempre mejor, como es lógico el exceso) y además hay que tener en cuenta que el testing se debería medir más por su efectividad y por su calidad que por el tiempo dedicado al mismo, si bien, se entiende que un equipo formado, aplicando una serie de metodologías y utilizando una serie de herramientas, mantendrá un nivel de efectividad más o menos regular por unidad de tiempo.

A lo anterior hay que añadir que el testing se debe realizar en todo el proceso de desarrollo y no solo al final y que el mismo equipo de proyecto debe participar en el mismo mediante la aplicación de pruebas unitarias, integración continua, pruebas funcionales, verificaciones continuas con el usuario y obtención e implementación del feedback, etc…, independientemente de que existan una serie de profesionales que sean especialistas en realizar este tipo de trabajo.

El testing es muy importante y no una disciplina secundaria, no es algo que se debiera infravalorar, como en el mundo de la programación hay programadores buenos, malos y regulares, lo mismo pasa con los testers. Un producto que llega a producción con errores graves va a costar dinero, de una u otra forma. En algunos casos estos errores serán críticos y el coste será elevadísimo en otros casos provocará un parón en el servicio que no es poco.

Resulta muy interesante la siguiente cita de Weinberg porque refleja bien a las claras lo crítico que resulta en muchas ocasiones el proceso de testing y lo complicado que resulta establecer sus límites (traducción libre): “En septiembre de 1962, saltó a la luz una noticia que indicaba que un cohete de 18 millones de dolares había sido destruido en pleno vuelo debido a un simple guión que faltaba en un programa. La naturaleza de la programación es así, ya que no existe relación entre el tamaño del error y los problemas que causa. Por tanto, es difícil definir cualquier objetivo en el proceso de testing, sin llegar a la eliminación de todos los errores, algo que resulta imposible”.

Detectar errores al final puede ser demasiado tarde. Si las fallas son funcionales, la arquitectura no es buena, la codificación deja mucho que desear y la culpa es tuya (en muchos casos aunque no lo sea, si el cliente tiene fuerza y no te respeta) el esfuerzo de corregirlo será tan importante que los beneficios del proyectos, si los hay, se diluirán y si hay pérdidas se multiplicarán.

El testing debe acompañar al proyecto desde que se concibe. Los que nos dedicamos a esto tenemos el defecto de que damos demasiadas cosas por supuesto y nos pasan demasiadas cosas por eso. Desde el plan de proyecto, desde el primer acta de reunión, todo debe ser revisado, todo debe ser validado.

¿Es eso ágil? Habrá quien piense que no, pero desde mi punto de vista no hay nada más ágil que no tener que volver a hacer algo de nuevo sin que el usuario te haya dicho que lo cambies. ¿Qué hay que hacer quince veces una pantalla? Pues se hace, si con ello y en cada paso estamos más cerca de que el usuario vea satisfechas sus expectativas. Evidentemente eso cuesta dinero y si no hay presupuesto detrás, el usuario deberá intentar ser más preciso (o renunciar a funcionalidades menos prioritarias).

En el proceso de codificación las pruebas deben hacerse cuanto antes. Si consideras que TDD es demasiado, aplica pruebas unitarias una vez desarrollado el código, si también consideras que eso es demasiado aplica la técnica que prefieras, pero por favor, prueba lo que estés haciendo y no des por supuesto que determinadas partes del código funcionan salvo que estés muy seguro.

¿Cuánto tiempo dedica tu equipo de proyecto a realizar testing?, ¿cuál es la proporción respecto al esfuerzo dedicado netamente a desarrollar?, ¿se utilizan estrategias que automaticen determinados tipos de test como por ejemplo las pruebas unitarias?, ¿vas más allá y aplicas TDD?, ¿usas integración continua?, ¿en qué etapas realizas pruebas?, ¿comienzan desde el mismo proceso de análisis?, ¿cuál es la participación del usuario (o el cliente en general) en las mismas?, ¿en qué proporción es explotaratoria y en qué proporción se basa estrictamente en casos de prueba?, ¿participan personas distintas al equipo de proyecto en pruebas de tipo funcional?, ¿se realiza testing de seguridad, usabilidad, rendimiento?, dicho testing, ¿cuánto es manual y cuánto está asistido por herramientas?, ¿no se realizan pruebas de la aplicación en un entorno de integración del cliente?, ¿realizas análisis estático de código?, en dicho análisis, ¿tienes definidos unos umbrales mínimos de calidad exigibles a las distintas métricas?, cuando vas a entregar una nueva versión de un producto software, ¿haces pruebas de regresión?.

Aunque tal vez todo lo anterior se pueda resumir en par de preguntas, ¿qué nivel de importancia le das al testing en las aplicaciones que desarrollas?, ¿tienes mecanismos para comprobar si se realiza testing y para evaluar la efectividad del mismo?.

El testing se infravalora de manera muy injusta. Bien realizado ahorra muchos problemas y por encima de eso, permite conservar tu imagen y tu dinero.

Hay muchos que piensan que el testing no es ágil y yo respeto las opiniones de cada uno, independientemente de que las comparta o no. Como he dicho muchas veces, lo que no es ágil es tener que repetir trabajo no por evolucionar una funcionalidad o un componente software, sino para corregir errores que perfectamente detectables en etapas anteriores.

Es más ágil (y menos arriesgado) prevenir un incendio que intentar sofocarlo después.

Boris Beizer es un autor e ingeniero de software americano (aunque nacido en Bélgica) especializado en el campo de la calidad del software y del testing. Sus primeras publicaciones datan de finales de la década de los cincuenta, aunque su etapa más prolífica fue en las décadas de los setenta y de los ochenta.

El testing es un campo dentro de la ingeniería del software un tanto controvertido. La mayoría de los profesionales consideramos necesarias las pruebas en el proyecto de desarrollo de software, si bien no hay coincidencia en cuanto a la intensidad, los momentos y la metodología.

Soy de la opinión de que no todos los proyectos y sistemas de información deben tener un mismo tratamiento y que independientemente de que exista una cierta metodología en las pruebas, estas deben girar alrededor del testing ágil y del exploratorio.

También soy partidario de que las clases con mayor acoplamiento sean cubiertas mediante pruebas unitarias (no necesariamente desarrollando según técnicas de desarrollo guiadas por las pruebas (TDD)) y que se deben minimizar los efectos colaterales a través de las pruebas de regresión.

En cuanto a los momentos, el testing debe aplicarse desde las etapas más tempranas del software, si bien los actores pueden ser distintos a las pruebas realizadas sobre el producto.

Y como estrategia, la utilización de integración continua permite detectar problemas de carácter unitario y de integración lo antes posible y a reducir los problemas en la fase de implantación.

Boris Beizer realiza la siguiente reflexión (traducción libre): “Una amenaza bien vale mil pruebas”.

Desgraciadamente nos acordamos de un proceso de testing bien realizado cuando ya los errores han salido a la luz y lo peor de eso, cuando su corrección requiere de un esfuerzo considerable, cuando ha impactado gravemente en el negocio o cuando nos crea un problema.

Cuando mayor sea la criticidad del sistema más peso debe tener el proceso de testing y más peso debe cobrar la metodología y sistematización.

En el artículo anterior vimos las actividades que definían al proceso de implantación y de las consecuencias de que el tiempo necesario para realizar esa actividad superasen unos umbrales admisibles para el proyecto de desarrollo de software en cuestión.

La aplicación de una metodología o enfoque ágil o la utilización, en general, de alternativas iterativas e incrementales suponen por su propia naturaleza una disminución de los tiempos de implantación, sobre todo, una vez que ya se han realizado varias evoluciones del sistema.

Sin embargo, si el tiempo necesario para realizar las implantaciones resulta superior al tiempo de iteración definido tenemos un problema ya que provoca una acumulación de las entregas y que buena parte del tiempo invertido en el proceso de implantación se pierda. Esta situación no debería mantenerse mucho tiempo y para solucionarla sería necesario retrasar una entrega hasta que se pueda subir una acumulación de evoluciones a producción.

Estas situaciones pueden producirse en más ocasiones de las que podemos pensar, ya que lo mismo los trabajos siguiendo metodologías ágiles, parten de una versión del producto avanzada y que no ha sido implantada todavía (o no se han implantado con éxito), los equipos encargados de testing y de paso a producción tienen una carga de trabajo elevada, otras prioridades, etc…

Soy de la opinión de que para poder trabajar de manera adecuada hay que estabilizar la situación, es decir, si la implantación de un producto resulta compleja o está dando problemas, hay que centrar los esfuerzos en ella y adaptar los desarrollos que se están haciendo a esa circunstancia.

Además de la aplicación de enfoques ágiles, ayuda mucho la existencia de unos entornos de integración y de preproducción los más parecidos posibles a los de reales, la aplicación de la integración continua, la utilización de técnicas de desarrollo guiadas por las pruebas (TDD) o al menos la aplicación de una estrategia adecuada de aplicación de pruebas unitarias, para reducir de esta forma la aparición de efectos colaterales y disminuir los tiempos necesarios para las pruebas de regresión y la corrección de este tipo de incidencias.

Además de lo anterior es importante que con carácter previo a la entrega el producto haya sido probado de manera adecuada por el equipo de proyecto (nada de entregas a ciegas o de pruebas superficiales).

También resulta muy adecuado el establecimiento de itinerarios de testing (todas las aplicaciones no requieren el mismo nivel de testing, ni todas las circunstancias son similares), la aplicación de testing ágil, proporcionar a los proveedores información detallada sobre las características de nuestro entorno de producción, la aplicación de una política de prioridades a los equipos encargados del paso a producción acorde a la problemática existente en cada momento, etc…

Martin Fowler creó el concepto de integración continua como una técnica o estrategia que consiste en programar automáticamente el proceso de compilación y ejecución de pruebas unitarias de un software que se está desarrollando.

El objetivo de la técnica consiste en detectar errores unitarios y de integración lo antes posible. Si se dejan estas comprobaciones para etapas muy tardías del proyecto los costes de corrección de estas incidencias se disparan) sobre todo en equipos de proyecto de tamaño medio o grande que trabajan en diferentes localizaciones).

No es una estrategia exclusiva de metodologías ágiles (podría ser utilizada perfectamente en metodologías del proceso unificado como RUP o en la fase de construcción de metodologías clásicas como por ejemplo el ciclo de vida en cascada), si bien su aplicación en las mismas resulta de gran importancia tanto en el proceso de desarrollo como para reducir los tiempos de implantación del sistema.

Se entiende integración continua cuando el proceso se programa para que se ejecute automáticamente. También podría considerarse cuando la integración se realice de forma planificada, aunque se realice de forma manual, en intervalos razonablemente cortos de tiempo. No es integración continua si se ejecuta de manera esporádica o en momentos próximos a la entrega.

En el entorno de personas con las que trabajo existen serias dudas de que la metodología de desarrollo guiado por las pruebas o Test-Driven Development, sea ágil, en el sentido de que entienden que si se aplica de manera ortodoxa o si bien las pruebas unitarias se desarrollan después, se pierde capacidad de trabajo efectivo por parte del equipo de proyecto.

Nunca he participado en un proyecto que siga esta metodología y en los desarrollos en los que he trabajado la importancia que se le da a las pruebas unitarias es muy escasa, por lo que no puedo aportar mi experiencia para opinar sobre las bondades o no de esta metodología, de manera que me tendré que centrar en lo que he leído sobre ella.

No obstante, hay que tener en cuenta una cosa. Agilidad no es lo mismo que desarrollar más rápido o por lo menos no es su objetivo principal, ya que si bien es cierto que la aplicación de metodologías ágiles, eliminan muchos aspectos que son prescindibles en un proyecto de desarrollo de software y por lo tanto pueden optimizar tiempos de desarrollo, el objetivo último de las mismas es conseguir productos software de calidad, que proporcionen satisfacción al cliente, dentro de unos plazos y presupuestos definidos (siempre y cuando, ambos sean realistas y acordes a la naturaleza del proyecto y al nivel de calidad que se espera del producto software), es decir, que no se produzcan los factores que derivan en la crisis del software.

El desarrollo guiado por las pruebas, fue introducido (o reintroducido) por Kent Beck, creador de la metodología de programación extrema y en cierto modo la segunda es una consecuencia natural de la primera, si bien ambas pueden utilizarse sin problemas por separado.

La metodología TDD a grandes rasgos es fácil de exponer:

– Se construyen las pruebas unitarias (o se puede extender si se desea el concepto a otros tipos de pruebas de más alto nivel, siempre y cuando sea automatizable su ejecución).

Como estos casos de prueba están construidos antes desarrollarse el componente software sobre el que se va a trabajar, van a fallar a la primera. De hecho se considera necesario verificar que fallan porque si no es así, querría decir que el test está más construido (¿por qué no?) o que el componente software ya está desarrollado (en proyectos donde se trabaja con sistemas de mediana o gran envergadura y con un número de desarrolladores alto, puede pasar que el componente o módulo ya se haya implementado previamente, bien porque le ha hecho falta a un compañero para otra funcionalidad o bien porque no se ha verificado que efectivamente ese componente ya estaba desarrollado).

Para la construcción de los casos de prueba, los desarrolladores deben basarse en las especificaciones indicadas en los casos de uso o en las historias de usuario. Ahí radica la importancia que tiene la construcción de las pruebas unitarias antes del desarrollo del componente, ya que implica un mayor enfoque en los requisitos, una mayor necesidad de comprender la funcionalidad y comportamiento deseado, es decir, implica comprender el problema, conocer los resultados esperados, antes de empezar a desarrollarlo.

La ventaja de desarrollar las pruebas antes de implementar los componentes es la indicada en el párrafo anterior, es decir, enfrentarse y comprender el problema antes de empezar a resolverlo.

Si las pruebas unitarias se construyen después, no estaríamos hablando de TDD, eso es importante, por lo que lo repito, si las pruebas unitarias se construyen después se trata de otra estrategia de desarrollo de software, no de desarrollo guiado por las pruebas.

Pese a que como he comentado, no se ha fomentado en los proyectos en los que he participado la construcción de pruebas unitarias, soy de la opinión de que es favorable tener una buena cobertura de código, por lo que la existencia de pruebas unitarias, antes o después de implementar los componentes, permite detectar de manera rápida la existencia de efectos colaterales o desarrollos erróneos de métodos o clases.

¿Por qué no lo hemos fomentado? Es una suma de varios factores, por un lado pese a que entendemos los beneficios que puede proporcionar, no tenemos del todo claro su retorno de la inversión, pese a que es objetivo que la deuda técnica se reduce en proporción a la cobertura, por otro lado, los proveedores con los que hemos trabajado tampoco utilizan esta práctica en sus desarrollos. Por tanto y en resumen, la falta de experiencia nuestra y de los proveedores hace que nos más dar el paso y prioricemos otros aspectos en el proceso de desarrollo y en la calidad del diseño y de la codificación.

– A continuación se construye el componente software que debe verificar las reglas de funcionamiento definidas en las pruebas.

Una vez desarrollado, se pasa el test y si funciona adecuadamente, se pasa de nuevo todo el juego de pruebas, si hay errores, se vuelve a revisar el componente, si todo ha ido bien, se pasa a la siguiente fase.

– Una vez que se tiene un software que verifica las pruebas implementadas, se procede a su refactorización.

El objetivo de la utilización de esta técnica es evidente, desarrollar software con el menor número de fallos posible, reduciendo además los errores debidos a efectos colaterales en el desarrollo, respetando también la calidad de diseño y de codificación para favorecer la comprensión y capacidad de mantenimiento del software.

Se sabe que cuanto más tarde se detecten los errores más esfuerzo se requiere para solucionarlo, esta técnica pretende disminuir el riesgo por la presencia de errores de ese tipo, además de facilitar un código donde sea mucho más fácil trabajar, tanto para corregir esas incidencias como para contruir nuevas funcionalidades o modificar, si fuera necesario (el software es de naturaleza adaptativa), las ya implementadas.

Espero poder probar más pronto que tarde esta metodología en alguno de los proyectos en los que participo y poder verificar personalmente las ventajas que tiene la utilización de esta estrategia de desarrollo.

Las técnicas de testing de caja blanca se realiza cuando el tester accede al código fuente de la aplicación y en consecuencia a los diferentes algoritmos y estructuras de datos utilizadas. La implementación de este tipo de pruebas requiere habilidades de programación, un conocimiento del framework de desarrollo y un cierto conocimiento funcional que permita conocer qué misión tienen determinadas clases y métodos.

Entre las técnicas de testing de caja blanca más conocidas tenemos la cobertura que consiste básicamente en la verificación de que todos los caminos lógicos de la aplicación son alcanzables teóricamente en función de los diferentes valores de entradas de los parámetros. Este tipo de pruebas, se automatizan con la ejecución de pruebas unitarias.

Otra técnica bastante conocida es la Mutation Testing que se suele utilizar para verificar la bondad de los métodos de testing utilizados. Se basa principalmente en realizar ligeras modificaciones en el programa que darían lugar a un comportamiento anómalo del mismo (resultados distintos) y verificar si la estrategia de testing utilizada es capaz de detectar estos cambios. Ejemplos de estos pequeños cambios lo podemos tener modificando el operador en las guardas de sentencias selectivas o iterativas, eliminando sentencias, etc…

Entre las críticas a esta estrategia de testing se encuentra el hecho de que hay que saber elegir muy bien cómo realizar las mutaciones, ya que hay que verificar que realmente lo que se cambia provoca un comportamiento diferente en el algoritmo porque lo mismo haces un cambio y no cambia de comportamiento.

Otra técnica de caja blanca la tenemos con la Fault Injection que persigue objetivos similares a la Mutation Testing (de hecho se considera a esta técnica como parte de la Fault Injection). Entre este tipo de técnicas tenemos la Code Insertion Testing, en la cual se introducen sentencias nuevas que provocan un error o un comportamiento anómalo en el sistema. Además de estas tećnicas, que se aplican en tiempo de compilación, la Fault Injection también aplica estrategias en tiempo de ejecución, como el hecho de provocar determinados fallos o excepciones del sistema que permitan estudiar el comportamiento del sistema en esos casos.

Una de las técnicas de testing de caja blanca más conocida es el análisis estático de código que tiene como objetivo principal evaluar (directa o indirectamente) la deuda técnica del software o lo que es lo mismo, evaluar el grado de mantenibilidad del sistema.

La mantenibilidad del sistema es una característica a la que no se suele dedicar suficiente atención, al fin y al cabo lo importante es que el sistema funcione (algo de lo que estoy totalmente de acuerdo), pero que una vez que se consigue debe ser compatible con el hecho de que el sistema sea escalable y pueda ser modificado a un coste razonable.

Además, por regla general, un sistema con una deuda técnica elevada será más propenso a tener errores.

Ver también:

Testing de caja negra.
Testing de caja gris.