archivo

Archivo de la etiqueta: pruebas unitarias

Sistemas altamente acoplados, con clases y métodos kilométricos, con código que resulta complicado de entender son ejemplos en donde realmente se programa con miedo una vez que se comprueba que tocarlo supone implicaciones hasta en las partes más insospechadas de la aplicación.

Si el sistema además es crítico el miedo pasa a convertirse en pánico.

Tal vez las palabras miedo o pánico sean demasiado exageradas (y no es nada positivo trabajar de esa manera) pero por lo menos el programador debe sentir respeto y estar alerta cuando realiza modificaciones en un sistema de este tipo. El exceso de confianza no es bueno y más en un caso como este.

Hay que evaluar si el cambio que se va a hacer en el sistema es algo puntual o si se sabe que van a existir una serie de cambios a corto y medio plazo, así como la magnitud de los mismos y la magnitud del sistema. El esfuerzo que supone realizar estos cambios (mucho mayor que en un sistema con una deuda técnica ajustada a sus características), el esfuerzo en testing y el riesgo que tiene liberar nuevas versiones de estos sistemas debe ser analizado antes de afrontar la mejor estrategia para el cambio.

En condiciones como esta, cambios que sean fruto de un capricho no deben ser contemplados y cambios muy relevantes que supongan una modificación sustancial del sistema podrían dar lugar a que se tome la decisión de desarrollar una nueva aplicación.

El programador por su parte puede tomar sus precauciones: testing unitario, automatización de determinado testing de mayor nivel, etc…

Por otro lado, el paso a producción de nuevas versiones de sistemas de estas características no debe tratarse a la ligera y esperar cruzando los dedos a que no pase nada.

El testing debe ser realizado en profundidad y desde el punto de vista funcional debería ser la combinación de casos de prueba y testing exploratorio.

Hay muchos desarrolladores que aplican TDD en sus desarrollos o, al menos, desarrollan pruebas unitarias para el software que están implementando.

Sobre ambos aspectos TDD (las pruebas unitarias se construyen antes) o las pruebas unitarias en general hay defensores y detractores y como suele suceder en tantos aspectos, no hay una verdad absoluta, sino que la aplicación o no de estas estrategias dependen del contexto en que se realiza el desarrollo, del tipo de sistema y del equipo de programadores.

Pese a que hablo de contexto incluso en situaciones favorables existen reticencias por muchos desarrolladores que lo consideran una pérdida de tiempo o una carga que llevar encima desde que se implementan, sin embargo quienes lo utilizan y lo consideran como una actividad más dentro de la programación lo suelen aplicar prácticamente en cualquier contexto.

No voy a defender a unos o a otros, cada cual tiene su criterio. Desde mi punto de vista la posibilidad de realizar testing unitario y su automatización supuso en su día una revolución en el desarrollo de software y en el mantenimiento de sistemas porque todo lo que sea establecer controles que permitan construir sistemas más robustos y entregar productos con menos errores debería ser bienvenido

Martin Fowler como buen defensor de estas prácticas realizó la siguiente reflexión sobre los frameworks de pruebas unitarias: «Nunca en el campo de desarrollo de software tantos le debieron tanto a tan pocas líneas de código».

La resistencia vista desde distintas perspectivas, como por ejemplo el tiempo de más que tarda en pasarse a producción un producto o todas aquellas dificultades u obstáculos extras en el proceso de desarrollo, que puede ir desde la deuda técnica hasta la falta de colaboración por parte de los usuarios, dificulta el establecimiento de ciclos cortos en un enfoque iterativo e incremental.

Esto al final es como la pescadilla que se muerde la cola, ya que cuanto más largo sea el ciclo, mayor número de funcionalidades se incorporarán o se modificarán y alimentarán, por ejemplo un paso a producción más largo.

No entro a valorar en término cuantitativo qué es un ciclo corto o qué es un ciclo largo porque opiniones hay para todos los gustos, como también las hay para determinar si todas las iteraciones dentro de un proyecto deben tener la misma duración o no.

Si se quieren ciclos cortos y que estos sean efectivos (que sean resolutivos de cara a las expectativas del usuario) hay que disminuir la resistencia y eso no es una cuestión simple, sobre todo si se parte de la base de que se intenta aplicar este enfoque de desarrollo en proyectos que se encuentran en una etapa avanzada o que se encuentran en fase de mantenimiento.

Disminuir la resistencia debe ser siempre un objetivo a tener presente, sobre todo condiciona de manera grave el proceso de desarrollo. Si es importante en todos los casos, lo es todavía más cuando se pretende liberar nuevas versiones del producto cada poco tiempo.

Disminuir la resistencia implicará actuaciones no funcionales (y eso será necesario tenerlo en cuenta y explicarlo muy bien al usuario), como por ejemplo automatizar en lo posible la instalación de la aplicación en los diferentes entornos, la automatización en la ejecución de las pruebas partiendo desde las pruebas unitarias y subiendo cuantos niveles sean precisos, la realización de pruebas manuales en aquellos casos donde no llegue o no sea rentable automatización, la refactorización, etc…

También obligará a realizar diversas tareas de gestión para intentar agilizar los procesos y las actividades de las diferentes personas y roles que participan en el proceso de desarrollo y de implantación y aceptación del sistema.

La programación defensiva es el resultado de la aplicación de una serie de técnicas que tienen como objetivo minimizar el número de errores que puede presentar un sistema de información, así como presentar una arquitectura y código que además de servir de soporte al objetivo anterior permita realizar un mantenimiento del sistema de la forma más limpia posible y minimizando o eliminando posibles efectos colaterales.

Aplicar esta técnica requiere incrementar notablemente el número de controles y comprobaciones que se realizan desde el código ya que se querrán dar respuesta al mayor número de casuísticas posibles, así como ser muy metódico en la aplicación de pruebas unitarias, pruebas de integración y pruebas de sistema desde etapas muy tempranas del desarrollo.

Todo lo anterior sin olvidarnos de las correspondientes pruebas de seguridad y pruebas de rendimiento.

Este testing será realizado tanto por el equipo de proyecto como por uno o varios equipos externos los cuales inclusos pueden estar especializados en un área concreta de testing.

De igual forma se requerirá que las especificaciones estén lo más cerradas y validadas posibles por el cliente o por el usuario, en este caso, el margen de error y la aproximación mediante la combinación feedback e iteraciones es mucho menor, por lo menos a nivel del producto en producción, donde deberá entrar con plenas garantías de acabado y funcionamiento.

También, el uso de componentes externos o de la delegación de competencias en terceros sistemas requerirá tener de los mismos unas garantías similares a la del sistema que se está desarrollando (toda cadena se rompe por el eslabón más débil).

En sistemas considerados críticos, donde se ponga en juego la vida o la integridad de las personas, así como aquellos que pueden afectar de manera sensible al funcionamiento de una organización y a su economía, la aplicación de este tipo de estrategias cobra especial sentido.

Es importante tener en cuenta que no solo es cuestión de técnicas, también es cuestión de un cambio de actitud cuando nos enfrentamos a este tipo de desarrollos, por eso es importante que un porcentaje elevado de personas que participan en estos proyectos tengan experiencia en los mismos, así como que ocupen todos los puestos críticos del proyecto.

Se han realizado estudios que indican que más del 50% de la corrección de errores introducen errores adicionales en el código.

Por este motivo resulta fundamental la aplicación de buenas prácticas en la codificación, la utilización de una buena arquitectura y framework, la aplicación de pruebas unitarias y de integración continua y la realización de nuevas pruebas una vez que se haya integrado un componente corregido.

Por esto es tan importante que el proceso de desarrollo no sea algo improvisado sino algo reglado, donde cada miembro del equipo de proyecto conozca qué es lo que tiene que hacer y qué técnicas tiene que utilizar para programar.

Barry Boehm es una buena referencia a la hora de considerar como adecuados determinados indicadores obtenidos en sus investigaciones y proyectos, no en vano siempre ha sido considerado como una de las referencias en el campo de la ingeniería del software, metodologías de desarrollo, estimación de costes y obtención y determinación de métricas relacionadas con el proceso de desarrollo.

Independientemente del indicador que cita Boehm y que indicaré al final de artículo, la propia experiencia nos dice que el coste de corrección de un error se dispara conforme nos alejamos del momento en que se ha producido. Esto es así porque un error arrastra tras de sí otros trabajos que correctos o no, se verán alterados por la corrección del mismo. Por ejemplo, una mala especificación de un requisito que llega a producción tendrá repercusión en el producto final, pero también su corrección requerirá un esfuerzo considerable por los diferentes artefactos que se tienen que modificar o incluso rehacer.

La aparición de los modelos de testing en V o de testing en W son una respuesta posible ante esos problemas, como también lo son la aplicación de otras técnicas en el proceso de construcción, como por ejemplo pruebas unitarias, integración continua, etc…

No solo afectó esta circunstancia a la aparición de modelos o técnicas de testing, sino que las metodologías de una u otra manera quedan impregnadas por la necesidad de detectar y corregir cuanto antes los errores (de hecho el propio testing en V no es más que una variante del ciclo de vida clásico o en cascada) como por ejemplo podemos ver en RUP o por la propia dinámica de desarrollo siguiendo metodologías ágiles.

Sobre la detección tardía de errores y sus implicaciones en los costes de un proyecto, Barry Boehm comenta lo siguiente (en relación a errores en etapas muy tempranas en el desarrollo, como lo son la obtención de especificaciones o requisitos): «Corrige los errores de especificación cuanto antes. Corregirlos después, costará: un 500% más en la etapa de diseño, un 1000% en la etapa de codificación, un 2000% en la etapa de testing unitario y un 20000% más en la entrega».

Se trata de una técnica de testing de caja negra, en la que se definen diferentes hitos a ser probados, que pueden ir desde elementos de carácter unitario hasta elementos cuya funcionalidad es proporcionada por varias capas de la arquitectura de la aplicación, como por ejemplo, los campos de un formulario y a los que se les proporciona un juego de entrada de datos aleatorio que puede tener acotado o no el patrón de generación de los mismos.

En función del nivel de acotación (o de inteligencia) del juego de entrada de datos aleatorio generado, se distinguen diferentes tipos de Monkey testing: Dumb Monkey Testing, Brilliant Monkey Testing y Smart Monkey Testing.

El funcionamiento de un producto software se define mediante la integración de sus componentes internos y la integración con componentes externos.

En un ambiente de arquitectura del software donde la modularidad y la delegación de funcionalidades predomina sobre otros paradigmas y en donde los equipos de trabajo pueden estar en diferentes localizaciones geográficas, un gobierno adecuado de integración de componentes resulta fundamental para detectar cualquier anomalía lo antes posible.

CMMI no es ajeno a esta circunstancia y determina en su nivel 3 la necesidad de establecer una estrategia global. Perfectamente podría haber incluido a nivel de proyecto este proceso en el nivel 2, sin embargo la orientación del mismo a la administración o gestión de cómo se realiza al desarrollo y no a la definición de métodos de trabajo, es lo que ha hecho que este proceso al igual que otros pase a engrosar este nivel.

Por tanto, en este proceso se establece una metodología a nivel de organización en cuanto a la integración de componentes. Como en el resto de procesos, la definición de una estrategia global no consiste en la definición de un modelo único sino que permite escenarios en función de la tipología y características del proyecto.

En la descripción de este proceso se encuentra por un lado los pasos a seguir para preparar la integración (determinar la secuencia de integración, adecuar un entorno de integración y decidir cómo y a través de qué se realiza el proceso de integración), para garantizar la compatibilidad de las interfaces (mejor si se detectan problemas de este tipo antes de realizar la integración) y para realizar el propio proceso de integración (condiciones que tienen que cumplir los componentes con carácter previo a la integración, cómo realizar la misma y cómo evaluar el resultado final).

La implantación de este área de proceso no debe resultar complicada para la mayoría de las empresas de desarrollo de software ya que con carácter previo han tenido que gestionar de manera adecuada la gestión de la configuración (esencial para restar complejidad a este proceso) y porque generalmente cuentan con soluciones de integración de componentes y en muchos casos se aplican técnicas de integración continua y en menos, pero afortunadamente cada vez más, pruebas unitarias y técnicas rápidas de verificación estructural y funcional como smoke testing.

La definición de smoke testing es sencilla ya que son pruebas rápidas que se realizan sobre aspectos funcionales del software para comprobar a alto nivel si el mismo tiene el comportamiento esperado. Ahora bien, la forma y el momento de aplicación puede variar.

Lo más normal es que se aplique durante el proceso de construcción como un paso más allá a la integración continua y a las pruebas unitarias y pueden ser automáticas, manuales o mixtas, ya que lo que se pretende comprobar es si determinadas funcionalidades tienen el comportamiento adecuado.

Hay que tener en cuenta que el smoke testing más que descubrir errores concretos lo que pretende es verificar que a nivel funcional no se están produciendo incidencias graves.

En otros contextos se aplica smoke testing con caracter previo a pruebas funcionales más exhaustivas con el objeto verificar si merece la pena ponerse con ellas, es decir, si se detectan errores graves (el sistema no funciona, sea cae a la más mínima operación, no abre una pantalla que permite acceder a una funcionalidad esencial, etc…), ¿para qué perder el tiempo haciendo un testing en mayor profundidad del sistema?

También es frecuente aplicar este tipo de testing con carácter previo a una entrega al cliente, de manera que se realice una última comprobación para verificar que a alto nivel todo está bien (antes como es lógico se debería haber realizado unas pruebas en profundidad del sistema de información a todos los niveles, no solo el funcional).

Bogdan Bereza-Jarocinski es un desarrollador de software y psicólogo polaco especializado en la calidad del software, más concretamente en el campo del testing. No obstante, ha pasado prácticamente por los diferentes perfiles clásicos por los que suele pasar un desarrollador: programador, analista, jefe de proyectos…

Me ha resultado interesante una reflexión que realizó en el año 2000, sobre el proceso de automatización del testing (traducción libre): «Introducirse en la automatización de las pruebas es, a veces, como un romance: tormentoso, emocional, en ocasiones un fracaso y en otras un éxito espectacular».

Esta cita puede ser válida en muchos campos del desarrollo de software cuando existe un cambio de la manera en que tradicionalmente se han hecho las cosas a otra diferente: nunca es fácil, genera un cierto desgaste, siempre aprendes y el resultado puede ser bueno o nefasto.

La automatización del testing, en todos los niveles, no solo a nivel de la definición de pruebas unitarias o la aplicación de técnicas orientadas a las mismas como TDD, supone un cambio de mentalidad importante en el desarrollo de software, ya que es la consecuencia de entender, por fin, que:

– El software es de naturaleza adaptativa o evolutiva, donde los mismos usuarios van refinando con el uso del producto la idea que tenían inicialmente del mismo y donde los mismos procesos de negocio evolucionan, dando lugar a nuevas versiones de la aplicación.

– Donde la calidad del software es un objetivo esencial a conseguir por cualquier desarrollador y que parte de esa calidad final depende de la capacidad de localizar errores en las etapas más tempranas posibles (para reducir los costes asociados a su corrección) y para reducir el número y criticidad de los mismos que llegan a entornos de producción.