Aplicación de La programación orientada a aspectos en el diseño e implementación de pruebas funcionales

Posted on 22 abril 2008. Filed under: Pruebas Unitarias |

Creadores del documento: Javier J. Gutiérrez1, Darío Villadiego1, María J. Escalona2, Manuel Mejías2

Departamento de Lenguajes y Sistemas Informáticos – Universidad de Sevilla dariovifer@yahoo.es, risoto@lsi.us.es

RESUMEN

En este trabajo exponemos cómo aplicar la programación orientada a aspectos para desarrollar pruebas unitarias. Primero presentamos la estrategia más utilizada para diseñar y escribir estas pruebas y, a continuación, identificamos y describimos un conjunto de escenarios donde esta estrategia no es adecuada. Para cada uno de estos escenarios se propone la aplicación de técnicas de programación orientada a aspectos para la realización de pruebas funcionales adecuadas. Por último desarrollamos un ejemplo práctico donde aparecen varios de estos escenarios e implementamos la solución que la programación orientada a aspectos aporta.

PALABRAS CLAVES

Programación orientada a aspectos, AOP, pruebas unitarias, AspectJ.

1. INTRODUCCION

1.1. Introducción a la programación orientada a aspectos.

La programación orientada a aspectos [1] (a partir de ahora llamada AOP), fue presentada en público por Gregor Kickzales y su equipo de investigación de Palo Alto Research Center en 1996. Desde entonces ha tenido una gran presencia en entornos académicos y, actualmente, comienza a estar cada vez más presente en entornos de desarrollo reales.

La AOP es un nuevo paradigma cuyo objetivo es aumentar la modularización de los sistemas software. En concreto, propone extraer y centralizar en un solo punto los "crosscutting concepts", los cuales son fragmentos de código que deben repetirse en todos o la mayoría de módulos del sistema, como el registro de trazas o la persistencia de objetos. La AOP no rompe con las técnicas de programación orientadas a objetos sino que las complementa y extiende.

En la ilustración 1 se muestra la idea fundamental de este paradigma, consistente en centralizar en un solo punto todos los aspectos comunes a las clases que forman el sistema software.

clip_image002[6]

Ilustración 1. Evolución de un sistema orientado a objetos a un sistema orientado a aspectos.

A continuación, en los siguientes puntos, se enuncian brevemente los conceptos básicos de la AOP y se explica la manera más común de desarrollar pruebas unitarias sin aplicar AOP.

1.2. Conceptos básicos de AOP.

En la tabla 1 se describen brevemente algunos conceptos básicos sobre los que se asienta la AOP y que serán necesarios a lo largo de este trabajo:

Punto de unión(Join Point)

Una posición bien definida dentro del código orientado a objetos, por ejemplo, la declaración de un método.

Punto de ejecución (Advice)

Fragmento de código que se ejecuta cuando se activa un punto de corte.

Punto de corte(Pointcut):

Un conjunto de condiciones aplicadas a un punto de unión que, al cumplirse, activarán el punto de corte y se ejecutará el punto de ejecución asignado a dicho punto de corte.

Aspecto (Aspect)

La combinación de puntos de corte, puntos de unión y puntos de ejecución.

Tabla 1. Conceptos básicos de AOP.

1.3. Cómo se diseña una prueba unitaria.

Actualmente, en el proceso de desarrollo del software se ha aceptado como una técnica imprescindible el diseño e implementación de pruebas unitarias del código en desarrollo. Estas pruebas tienen como principales objetivos comprobar que el código se comporta de la manera esperada y prevenir la aparición de errores inesperados al modificar o refactorizar el código. También, en algunos casos como en el desarrollo dirigido por pruebas, las pruebas unitarias ayudan a obtener el diseño del código. Las ventajas de realizar este tipo de pruebas son: código más depurado, mayor seguridad a la hora de refactorizar y mayor velocidad de desarrollo, aparte de obtener código de mayor calidad.

Actualmente las pruebas unitarias se desarrollan con herramientas tipo jUnit [3] y objetos mock [4]. Las herramientas tipo jUnit se aplican para comprobar que el estado de un objeto, los valores retornados por los métodos, excepciones lanzadas, etc, son los esperados. Los objetos ficticios o mocks, en cambio, permiten aislar el código a probar de las dependencias con otras clases colaboradoras mediante el desarrollo de clases ficticias que simulan el comportamiento de estas clases colaboradoras. Esto permite centrar la prueba sólo en el código que se desea probar.

La estrategia para desarrollar este tipo de pruebas [4] consiste en extraer el comportamiento del colaborador a una interfaz y realizar dos implementaciones de la misma, una real que se utilizará en producción y una implementación ficticia, mediante un mock, que se utilizará en las pruebas unitarias. Para decidir la clase colaboradora a utilizar se puede añadir como parámetro del método.

Los elementos que intervienen en este tipo de pruebas y su relación se muestran en la ilustración 2.

clip_image004

Ilustración 2. Relación entre la clase bajo prueba y un colaborador.

Pero las pruebas unitarias no son siempre fáciles de desarrollar, por ejemplo cuando el comportamiento de un módulo depende en gran medida del estado del sistema en que es ejecutado. En el siguiente apartado comprobaremos como existen escenarios en los que esta estrategia presenta problemas a la hora de desarrollar pruebas unitarias y como podemos solventar estos problemas aplicando la AOP.

2. AOP Y PRUEBAS UNITARIAS.

2.1. AOP como complemento a pruebas unitarias con jUnit y Mocks.

Hemos visto una estrategia muy común para diseñar pruebas unitarias mediante el binomio de herramientas jUnit o similares y objetos mocks. Sin embargo, existen una serie de escenarios, por ejemplo en arquitecturas cliente/servidor o al trabajar con clases y métodos sin estado o estáticos, donde esta estrategia es insuficiente para diseñar e implementar pruebas unitarias. Este tipo de escenarios podemos solventarlos de una forma práctica y elegante aplicando la AOP al diseño e implementación de pruebas unitarias.

Estos escenarios se describen en los siguientes puntos junto con la aplicación de técnicas de AOP para solventarlos.

2.2.1. Dependencia de datos.

Un problema muy común al desarrollar pruebas unitarias es la dependencia de estas con los datos de prueba. Es necesario hacer las mínimas suposiciones posibles sobre los datos en los que basar las pruebas, por ejemplo un número de clientes determinado en una base de datos o la existencia de un cliente en concreto. Si no, algunas veces las pruebas se ejecutarán correctamente (cuando los datos reales coincidan con los esperados) y otras no, estando la causa del fallo no en el código bajo prueba sino en los datos que la prueba presupone.

Una primera solución, muy común, es que la propia prueba gestione los datos necesarios, por ejemplo insertando y borrando clientes determinados, pero esto no es siempre posible, por ejemplo por compartir un mismo almacén de datos para todo el equipo de desarrollo, porque podría aumentaría tanto la complejidad y el tiempo de ejecución de la prueba que la haría inviable o porque los datos dependen del sistema sobre el que se ejecuta el software.

Un ejemplo de esto último sería una aplicación que lance eventos según la hora del sistema. Si se quiere probar que el evento de las 6am se lanza adecuadamente sería necesario una manipulación manual del reloj del sistema, cosa que va en contra de la filosofía de la automatización de pruebas y no siempre será posible, por ejemplo si el código se ejecuta en un servidor en el que existen otras aplicaciones que también dependen del reloj del sistema para trabajar.

Para solucionar este escenario con técnicas de AOP, la estrategia a seguir será desarrollar aspectos cuya misión sea interceptar las llamadas a los métodos que soliciten los datos. En concreto, aplicado al ejemplo anterior, será necesario escribir un aspecto que intercepte las llamadas al método que devuelve la hora del sistema, si dicho método es invocado desde la clase encargada de probar que el evento de las 6am se lanza adecuadamente, el aspecto devuelve como hora del sistema 6am, independientemente de la hora real.

2.2.2. Pérdidas de rendimiento.

Hemos visto en el apartado 1.3 como la estrategia a la hora de desarrollar pruebas con jUnit y mocks es extraer las clases colaboradoras como parámetros de los métodos bajo prueba para poder alternar entre la clase real y la clase ficticia sin necesidad de modificar el código. Sin embargo, esto puede tener un impacto inaceptable sobre el rendimiento del sistema.

En arquitecturas cliente-servidor, como por ejemplo J2EE [5], aplicar esta estrategia a un método que será invocado remotamente implica tener realizar la serialización y posterior deserialización del objeto colaborador cada vez que se invoque al método, lo que, en la práctica, supone un coste tan alto que impide aplicar esta estrategia.

Una solución aplicando técnicas de AOP consistirá en interceptar la creación del objeto colaborador para devolver un objeto mock siempre que el método se invoque dentro del ámbito de una prueba.

2.2.3. Interacción entre métodos.

Las herramientas estilo jUnit necesita la existencia de un estado comprobable, por ejemplo cambios en los atributos de un objeto, un resultado devuelto por un método, una excepción lanzada, etc. Por esto es difícil elaborar pruebas unitarias con esta herramienta cuando no hay ningún estado comprobable.

Un ejemplo donde no existe un estado comprobable es en el caso en que existe un método encargado de almacenar los atributos de una clase en una base de datos y se impone como requisito la necesidad de reutilizar conexiones ya abiertas con la base de datos en vez de crear una nueva cada en cada llamada al método.

En este ejemplo es difícil elaborar una prueba que verifique que efectivamente se reutiliza una conexión ya existente en vez de crear una nueva, ya que, en ambos casos, el comportamiento del método, almacenar los valores de los atributos, es igualmente válido.

Aplicando técnicas de AOP es posible escribir una prueba que intercepte las llamadas y compruebe que se ha pedido una conexión ya existente en vez de crear una nueva.

2.2.4. Clases sin estado y métodos estáticos.

La estrategia para desarrollar pruebas con jUnit y mocks es extraer los métodos del colaborador a una interfaz y realizar dos implementaciones de ella: una real, utilizada en producción, y una ficticia mediante un objeto mock utilizada en el proceso de pruebas.

Sin embargo, en el caso de la plataforma Java, no es posible aplicar esta estrategia cuando las dependencias entre la clase bajo prueba y la clase colaboradora están basadas en llamadas a métodos estáticos o cuando la clase colaboradora es una clase sin estados, ya que la plataforma Java no permite interfaces con métodos estáticos.

La estrategia a seguir aplicando técnicas de AOP, será desarrollar un objeto mock basado en AOP que intercepte las llamadas a los métodos estáticos y simule el comportamiento de la clase real.

3. Un ejemplo práctico.

En este apartado se va desarrollar un ejemplo práctico que ilustra una situación en la que las pruebas unitarias basadas en jUnit y mocks no son suficientes para desarrollar correctamente las pruebas del código. A continuación se aplicarán técnicas de AOP para desarrollar una prueba unitaria adecuada.

3.1. Descripción del ejemplo.

Se desea desarrollar una prueba unitaria para verificar el correcto funcionamiento de un método llamado alerta() perteneciente a una clase llamada Control. Este método obtiene la temperatura de un sensor y devuelve verdadero o falso según la temperatura obtenida esté por encima o por debajo de un umbral especificado. Para obtener la temperatura del sensor el fabricante del mismo proporciona una clase sin estado (estática) llamada Sensor que dispone de un método llamado LeerTemperatura() que devuelve la temperatura actual del sensor.

En la ilustración 3 se detallan los detalles de ambas clases relevantes para este supuesto práctico.

clip_image006

Ilustración 3. Clases del ejemplo.

Todo el código, tanto las clases, las pruebas, como la clase suministrada por el fabricante, está implementado en lenguaje Java.

3.2. Por qué el binomio jUnit + mock falla.

Uno de los principios básicos para el diseño de pruebas unitarias es aislar el código que se desea probar de todas sus dependencias. Sin embargo, en este ejemplo aparecen dos de los escenarios que hemos identificado como problemáticos para el binomio jUnit+mocks que van a dificultar aplicar este principio. En primer lugar existe una dependencia de datos, ya que es imposible predecir que valor de temperatura va a devolver el sensor y por tanto, es imposible predecir cual va a ser el resultado del método alerta(), si debe activarse o no, por lo que no es posible comprobar si está trabajando correcta o incorrectamente.

Una solución es reemplazar la clase Sensor por un mock que devuelva una temperatura conocida a priori. Con este valor es posible determinar el comportamiento esperado del método alerta() y escribir una prueba que verifique si lo cumple o no. La estrategia a aplicar para trabajar con mocks, comentada en el apartado 1.3, consiste en evitar esta dependencia extrayendo los métodos que la clase Control necesita de Sensor a una interfaz y escribiendo dos implementaciones de la misma, una implementación real que será la propia clase Sensor y una implementación ficticia que devolverá valores conocidos de antemano, llamada SensorMock.

Aquí aparece otro de los escenarios identificados en el apartado anterior, ya que esta solución no puede ser aplicada si implementamos las clases en Java dado que este lenguaje no permite interfaces con métodos estáticos. Sin embargo aplicando técnicas de AOP sí va a ser posible diseñar una prueba con un mock de la clase Sensor creado con AOP, como se describe en el siguiente punto.

3.3. Aplicación de técnicas de AOP para el desarrollo de una prueba unitaria.

La estrategia a seguir en este caso, será crear una clase mock basada en AOP llamada AspectoSensorMock con un pointcut que enlace con las llamadas a la clase Sensor. El comportamiento asociado a este punto de corte consistirá en comprobar desde donde se lanzó la llamada y, si esta se lanzó desde el código de prueba, devolverá un valor establecido de antemano, o si se lanzó desde otro lugar, la llamada se ejecutará normalmente. Las clases y aspectos se muestran en la figura 4, utilizando la notación [6].

clip_image008

Ilustración 4. Clases y aspectos de la prueba.

Una posible implementación de esta estrategia, cuyo objetivo es interceptar la llamada si viene de la clase de prueba y devolver el valor 42, escrita para la herramienta AspectJ se muestra en tabla 2.

public aspect AspectoSensorMock {

pointcut inTest() : execution(public void ControlTest.*());

int around() :

call( public static int LeerTemperatura() )

&& cflow(inTest())

{

return 42;

}

}

Tabla 2. Mock de prueba del sensor escrito para AspectJ.

4. CONCLUSIONES y futuros trabajos.

4.1. Conclusiones.

En este trabajo se ha presentado una guía para aplicar las técnicas de AOP como complemento al desarrollo de pruebas unitarias y suplir las carencias de las herramientas más habitualmente empleadas en el desarrollo de este tipo de pruebas. Los escenarios identificados y las soluciones propuestas basadas en AOP demuestran que estas técnicas puede aplicarse satisfactoriamente para encontrar nuevas soluciones más sencillas y eficientes a la hora de desarrollar pruebas unitarias.

Aunque el hecho de tener que aprender un nuevo paradigma de programación y una nueva herramienta con su propio lenguaje de programación puede ser una barrera importante para los programadores a la hora de incorporar estas técnicas a un entorno de desarrollo real, las técnicas empleadas en este artículo son muy sencillas de aplicar y requieren adquirir muy pocos conocimientos nuevos. Su aplicación a entornos de desarrollo reales no requiere de grandes inversiones en tiempo, formación y recursos.

4.2. Futuros trabajos.

En este trabajo se han utilizado técnicas de AOP pero no lo esencial de AOP, que es el aumentar la modularización de los sistemas software extrayendo y centralizando en un sólo punto los fragmentos de código que deben repetirse en todos o la mayoría de módulos del sistema. Para corregir esto, se pretende continuar desarrollar otros enfoques de la AOP aplicada al proceso de prueba, como utilizar la AOP para reducir el número de pruebas unitarias necesarias incluyendo en el código aspectos que controlen elementos como precondiciones, invariantes, postcondiciones y restricciones, como, por ejemplo, que no se pueda acceder directamente a ningún atributo de ninguna clase, sino solo a través de una pareja de métodos set() y get().

REFERENCias

[1] D. Gradecki, Joseph; Lesiecki, Nicholas, 2.003. Mastering AspectJ Aspect-Oriented Programming in Java. Wiley & Sons, USA.

[2] Isberg, Wes, 2.002. Get Test-Inoculated!. Software Development Magazine, No. Mayo.

[3] jUnit: http://www.junit.org.

[4] Mackinnon, Tim; Freeman, Steve; Craig, Philip. Endo-Testing: Unit Testing with Mock Objects. eXtreme Programming and Flexible Processes in Software Engineering – XP2000.

[5] Alur, Deepak; Crupi, John; Malks, Dan. 2.003.J2EE PATTERNS core. Prentice Hall PTR, Upper Saddle River, NJ 07458, USA.

[6] Renaud Pawlak, et al. 2002. A UML Notation for Aspect-Oriented Software Design. Project ObjectWeb. http://www.objectweb.org/

Liked it here?
Why not try sites on the blogroll...

A %d blogueros les gusta esto: