Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Dr. Macario Polo Usaola Departamento de Informtica Paseo de la Universidad, 4 13071-Ciudad Real macario.polo@uclm.es
ndice
1. Introduccin.......................................................................................... 3 1.01 El proceso de pruebas en el ciclo de vida ..................................... 3 Pruebas de requisitos ............................................................ 4 Pruebas del diseo ................................................................ 5 Revisiones e inspecciones del cdigo fuente......................... 6
Pruebas estructurales o de caja blanca ............................................... 8 2.01 Criterios de cobertura.................................................................... 8 Cobertura de sentencias ........................................................ 8 Cobertura de decisiones ........................................................ 9 Cobertura de condiciones ...................................................... 9 Cobertura de condiciones mltiples ....................................... 9 Cobertura de condiciones/decisiones .................................... 9 Cobertura de caminos............................................................ 9 Cobertura de funciones.......................................................... 9 Cobertura de llamadas........................................................... 9 Cubrimiento de bucles ......................................................... 10 Cubrimiento de carrera ........................................................ 10 Cobertura de operadores relacionales ................................. 10 Cobertura de tablas.............................................................. 10
2.01.1 2.01.2 2.01.3 2.01.4 2.01.5 2.01.6 2.01.7 2.01.8 2.01.9 2.01.10 2.01.11 2.01.12 2.02
Pruebas funcionales o de caja negra ................................................. 17 3.01 Generacin de casos de prueba ................................................. 17 Secuencias de mtodos ....................................................... 17 Especificaciones formales o semiformales .......................... 18
3.01.1 3.01.2
Pruebas a partir de modelos ....................................................... 25 Diagramas de clases UML ................................................... 25 Criterios de cobertura de pruebas para diseos UML.......... 28 Diagramas de estados ......................................................... 29 En otros diagramas .............................................................. 29
Pruebas de componentes ........................................................... 31 Uso de BIT wrappers ........................................................... 31 Mutacin de interfaces ......................................................... 33
3.03.1 3.03.2 4. 5.
1. INTRODUCCIN
La fase de pruebas es una de las ms costosas del ciclo de vida software. En sentido estricto, deben realizarse pruebas de todos los artefactos generados durante la construccin de un producto, lo que incluye especificaciones de requisitos, casos de uso, diagramas de diversos tipos y, por supuesto, el cdigo fuente y el resto de productos que forman parte de la aplicacin (p.ej., la base de datos). Obviamente, se aplican diferentes tcnicas de prueba a cada tipo de producto software.
Del proceso de Verificacin se observa la importancia de verificar cada uno de los productos que se van construyendo, bajo la asuncin de que si lo que se va construyendo es todo ello correcto, tambin lo ser el producto final. Igualmente, se observa que el proceso de Validacin resalta la importancia de comprobar el cumplimiento de los objetivos de los requisitos y del sistema final, de suerte que podra construirse un Plan de pruebas de aceptacin desde el momento mismo de tener los requisitos, que sera comprobado al finalizar el proyecto. Si tras la fase de requisitos viniese una segunda de diseo a alto nivel del sistema, tambin podra prepararse un Plan de pruebas de integracin, que sera comprobado tras tener codificados los diferentes mdulos del sistema. Esta correspondencia entre fases del desarrollo y tipos de pruebas produce el llamado modelo en V, del que se muestra un ejemplo en la Figura 1.
Requisitos Pruebas de aceptacin
Pruebas de sistema
Pruebas de integracin
Cdigo
Pruebas unitarias
Figura 1. Modelo en V
1.01.1 Pruebas de requisitos La prueba de requisitos pretende comprobar los tres principales atributos de calidad de los requisitos, con el fin de detectar tantos errores como sea posible y cuanto antes: correccin (carencia de ambigedad), complecin (especificacin completa y clara del problema) y consistencia (que no haya requisitos contradictorios).
(Bashir and Goel 2000) proponen la utilizacin de una Matriz de Prueba de Requisitos (RTM: Requirements Testing Matrix), en la que se lista cada requisito junto a sus casos de uso y casos de prueba:
Requisito Casos de uso Casos de prueba Id del prototipo en que se incluye Validado con el usuario?
1.01.2 Pruebas del diseo La fase de diseo tiene como objetivo generar un conjunto de especificaciones completas del sistema que se va a implementar, transformando los requisitos en un Plan de implementacin. La prueba del diseo debe comprobar su consistencia, complecin, correccin, factibilidad (es decir, que el diseo sea realizable) y trazabilidad (es decir, que podamos navegar desde un requisito hasta el fragmento del diseo en que ste se encuentra). Las actividades que proponen (Bashir and Goel 2000) para este tipo de pruebas se muestran en la Figura 2.
Particionar requisitos
Revisar diagramas
Construir prototipo
1.01.3 Revisiones e inspecciones del cdigo fuente Las revisiones e inspecciones de cdigo fuente son una tcnica para la deteccin manual de errores en el cdigo (Bashir and Goel 2000). Se trabaja bajo el principio de que cuatro ojos ven ms que dos, de tal manera que el mtodo de trabajo consistir, bsicamente, en pasar el cdigo escrito por un programador a un tercero o grupo de terceros, que tratar de encontrar posibles errores, faltas de adecuacin al estilo de codificacin utilizado por la organizacin, etc. Para ello suelen utilizarse listas de comprobacin (checklists), que enumeran defectos y en los que el revisor anota su presencia o ausencia. En (Fox 1998) puede consultarse una lista de comprobacin de errores tpicos del lenguaje Java, entre los que se incluyen el control de overflows y underflows, restriccin adecuada del acceso a los miembros de las clases, control de apertura y cierre de ficheros, etc.
mientas de prueba, as como tener en cuenta que es imposible automatizar el 100% de las pruebas. Falta de soporte o comprensin por parte de los jefes, debido otra vez a la escasa importancia que habitualmente se le da a la fase de pruebas. Organizacin inadecuada del equipo de pruebas. Adquisicin de una herramienta inadecuada.
2.01.2 Cobertura de decisiones Comprueba el nmero de decisiones ejecutadas, considerando que se ha ejecutado una decisin cuando se han recorrido todas sus posible ramas (la que la hace true y la que la hace false, pero tambin todas las posibles ramas de un switch). 2.01.3 Cobertura de condiciones Comprueba el nmero de condiciones ejecutadas, entendiendo que se ha ejecutado una condicin cuando se han ejecutado todas sus posibles ramas. 2.01.4 Cobertura de condiciones mltiples Comprueba el nmero de condiciones mltiples ejecutadas, considerando que se ha ejecutado una condicin mltiple cuando se han ejecutado todas sus correspondientes ramas con todas las posibles variantes de la instruccin condicional. 2.01.5 Cobertura de condiciones/decisiones Comprueba el nmero de condiciones y decisiones que se han ejecutado. 2.01.6 Cobertura de caminos Comprueba el nmero de caminos linealmente independientes que se han ejecutado en el grafo de flujo de la unidad que se est probando. El nmero de caminos linealmente independientes coincide con la complejidad ciclomtica de McCabe. 2.01.7 Cobertura de funciones Comprueba el nmero de funciones y procedimientos que han sido llamados. 2.01.8 Cobertura de llamadas Comprueba el nmero de llamadas a funciones y procedimientos que se han ejecutado. No debe confundirse con la cobertura de funciones: en la cobertura de funciones contamos cuntas funciones de las que hay en nuestro pro-
grama han sido llamadas, mientras que la cobertura de llamadas cuenta cuntas de las llamadas a funciones que hay en el programa se han ejecutado. 2.01.9 Cubrimiento de bucles Comprueba el nmero de bucles que han sido ejecutados cero veces (excepto para bucles do..while), una vez y ms de una vez. 2.01.10 Cubrimiento de carrera
Comprueba el nmero de tareas o hilos que han ejecutado simultneamente el mismo bloque de cdigo. 2.01.11 Cobertura de operadores relacionales
Comprueba si se han ejecutado los valores lmite en los operadores relacionales (>, <, >=, <=), ya que se asume la hiptesis de que estas situaciones son propensas a errores. 2.01.12 Cobertura de tablas
2.02 Mutacin
Un mutante es una copia del programa que se est probando (programa original) al que se le ha introducido un nico y pequeo cambio (normalmente sintctico, como la sustitucin de un signo + por un *), lo que supone tener una versin defectuosa del programa original. Supongamos que ejecutamos un mismo caso de prueba en el programa original y en el mutante, y supongamos adems que la salida del programa original es correcta, y que la salida del mutante difiere en algo de la salida del original. Entonces, podremos asegurar que, para ese caso de prueba, la instruccin mutada es correcta en el programa original. Si a ambos programas les pasamos una batera de casos de prueba y siempre obtuviramos resultados correctos en el original e incorrectos en el mutante, podramos afirmar con poco temor a equivocarnos que la instruccin mutada de nuestro programa es correcta. Un mutante que produce que produce la misma salida que el programa original se dice que est vivo, y muerto si su salida es diferente. Cuantos ms mu-
10
tantes consigamos matar, estaremos logrando mayor cobertura del programa original, lo que de manera indirecta es una medida de la calidad del programa. 2.02.1 Proceso de pruebas basado en mutacin La Figura 3 muestra el proceso de mutacin propuesto por (Offut 1995). Las lneas punteadas indican tareas manuales, mientras que las de trazo continuo son tareas automatizables. Inicialmente se genera el conjunto de mutantes, el conjunto de casos de prueba y se define un umbral que representa el porcentaje mnimo de mutantes muertos que debe alcanzarse. Entonces se ejecutan los casos de prueba sobre el programa original y sobre los mutantes, y se calcula el porcentaje de mutantes muertos. Si no se ha alcanzado el umbral previamente definido, se eliminan los casos de prueba que no han matado mutantes y se generan casos de prueba nuevos, especialmente dirigidos a matar los mutantes que han permanecido vivos. Tradicionalmente, se considera que para matar a un mutante deben producirse tres condiciones: Alcance: la sentencia mutada debe ser ejecutada por el caso de prueba. Necesidad: entre la entrada y la salida del programa debe crearse un estado intermedio errneo. Suficiencia: el estado incorrecto debe propagarse hasta la salida del programa. La condicin de suficiencia puede ser difcil de conseguir en muchas ocasiones, lo que puede dificultar la obtencin de casos de prueba a un coste razonable. La mutacin dbil se queda en la condicin de necesidad para determinar que un caso de prueba ha matado a un mutante: compara el estado intermedio del mutante y el del programa original inmediatamente despus de haber ejecutado la instruccin mutada, marcando el mutante como muerto si los estados son diferentes. Se ha comprobado experimentalmente que esta tcnica puede ahorrar hasta el 50% del coste de las pruebas sin degradar excesivamente su calidad.
11
Progama original P
Crear mutantes M
Ejecutar T sobre P
Definir umbral
Corregir P
Se alcanz el umbral? S
No
P(T) es correcta? S
Fin
2.02.2 Operadores de mutacin La generacin de mutantes se consigue aplicando operadores de mutacin al cdigo fuente del programa que queremos probar. La Tabla 2 muestra algunos de los operadores de mutacin citados en (Offut, Rothermel et al. 1996).
Operador ABS ACR AOR CRP ROR RSR SDL UOI Descripcin Sustituir una variable por el valor absoluto de dicha variable Sustituir una referencia variable a un array por una constante Sustitucin de un operador aritmtico Sustitucin del valor de una constante Sustitucin de un operador relacional Sustitucin de la instruccin Return Eliminacin de una sentencia Insercin de operador unario (p.ej.: en lugar de x, poner x)
El nmero de mutantes que puede generarse a partir de un programa sencillo, de pocas lneas de cdigo, es muy grande (pinsese que el operador
12
AOR, por ejemplo, puede aplicarse a cada aparicin de un operador aritmtico en el programa original), resultando tambin muy costosas tanto la ejecucin de los casos de prueba con cada mutante como la comprobacin de la salida del programa. Por ello, se han realizado algunos estudios encaminados a disminuir los costes de este tipo de pruebas, evidencindose que, aplicando pocos operadores, se pueden conseguirse los mismos resultados que si se aplicaran muchos. Los operadores ms efectivos son el ABS, 0 (sustitucin de una variable por el valor 0), <0 (sustitucin de una variable por un valor menor que 0), >0 (sustitucin de una variable por un valor mayor que 0), AOR, ROR y UOI. Adems, tambin se han propuesto operadores especficos para programas escritos en lenguajes orientados a objeto, algunos de los cuales se muestran en la Tabla 3 (Kim, Clark et al. 2000).
Operador AMC (Access Modifier Change) AOC (Argument Order Change) CRT (Compatible Reference Type Replacement) EHC (Exception Handling Change) EHR (Exception Handgling Removal) HFA (Hiding Field variable Addition) MIR (Method Invocation Replacement) OMR (Overriding Method Removal) POC (Parameter Order Change) SMC (Static Modifier Change) Descripcin Reemplazo del modificador de acceso (por ejemplo: ponemos private en lugar de public) Cambio del orden de los argumentos pasados en la llamada a un mtodo (p.ej.: en lugar de Persona p=new Persona(Paco, Pil) poner Persona p=new Persona(Pil, Paco) Sustituir una referencia a una instancia de una clase por una referencia a una instancia de una clase compatible (p.ej.: en vez de poner Persona p=new Empleado(), poner Persona p=new Estudiante()). Cambiar una instruccin de manejo de excepciones (try...catch) por un sentencia que propague la excepcin (throw), y viceversa Eliminacin de una instruccin de manejo de excepciones Aadir en la subclase una variable con el mismo nombre que una variable de su superclase Reemplazar una llamada a un mtodo por una llamada a otra versin del mismo mtodo Eliminar en la subclase la redefinicin de un mtodo definido en una superclase Cambiar el orden de los parmetros en la declaracin de un mtodo (p.ej.: poner Persona(String apellidos, String nombre) en vez de Persona(String nombre, String apellidos) Aadir o eliminar el modificador static
Los operadores de mutacin deben ser aplicados segn el contexto del lugar del cdigo fuente en el que se vaya a realizar la mutacin. Si esto no se hiciera as, podran generarse multitud de mutantes funcionalmente equivalentes al programa original, otros que ni siquiera compilaran, etc.
13
14
Generar una solucin aleatoria como solucin actual Calcular coste de la solucin actual y almacenarlo como mejor coste Aadir la solucin actual como nueva solucin Aadir la nueva solucin a la lista tab do Calcular los vecinos candidatos Calcular el coste de los candidatos Almacenar el mejor candidato como nueva solucin Aadir la nueva solucin a la lista tab if coste de nueva solucin < mejor coste Almacenar nueva solucin como mejor solucin Almacenar coste de la nueva solucin como mejor coste end_if Almacenar nueva solucin como solucin actual while no se alcance el criterio de parada Figura 4. Algoritmo de bsqueda tab
Puesto que el objetivo de estos autores es alcanzar la mayor cobertura posible, utilizan un grafo que representa el flujo de control del programa, en cuyos nodos se anota si el propio nodo ha sido alcanzado, cuntas veces lo ha sido y cul es el mejor caso de prueba que lo ha alcanzado. Cuando no hay ramas inalcanzables, el mximo valor posible para la cobertura es el 100%, mientras que ser desconocido en caso de que las haya. Por este motivo establecen como criterio de parada o haber alcanzado todas las ramas, o que el algoritmo haya superado un nmero de iteraciones prefijado. Adems, cada solucin se caracteriza por su conjunto de valores de entrada. El coste de una solucin, fundamental para que el algoritmo funcione eficazmente, se calcula considerando que el mejor caso de prueba es aquel que tiene ms posibilidades de que sus vecinos permuten entre ramas o, lo que es lo mismo, aquel que alcanza el nodo con valores lmite. Por ejemplo, si la condicin es x!=y, la funcin de coste ser |x-y| (vase la referencia para conocer el resto de detalles de clculo de la funcin de coste). Para calcular los vecinos candidatos, los autores se basan en que, si un caso de prueba cubre al padre de un nodo pero no a su hijo, entonces puede encontrarse un vecino que alcance al hijo utilizando el caso que cubre al padre
15
a partir de la mejor solucin. A partir de sta generan 2n vecinos cercanos y 2n vecinos lejanos (donde n es el nmero de variables de entrada del programa). Los candidatos se comprueban frente a la lista tab, rechazndose aquellos que ya existen. En la siguiente iteracin se repite el proceso, con la diferencia de que el nodo objetivo puede haber cambiado si alguno de los candidatos alcanz el entonces nodo objetivo. 2.03.2 Algoritmos Genticos (I) (Pargas, Harrold et al. 1999) utilizan el algoritmo mostrado en la Figura 5 para lograr criterios de cobertura altos. En la primera lnea se calcula CDGPaths, que representa el grafo de dependencias de control del programa que se va a probar. Un grafo de dependencia de control es un grafo dirigido acclico cuyos nodos representan sentencias y cuyos arcos representan dependencias de control entre sentencias. Un nodo Y depende por control de otro X si y slo si: (1) cualquier nodo intermedio en el camino de X a Y est postdominado por Y; y (2) X no est postdominado por Y. Se dice adems que un nodo X est postdominado por otro Y si para llegar desde X a la salida es preciso pasar siempre por Y. A continuacin se inicializa Scoreboard, que guarda el registro de los requisitos de prueba (TestReq) satisfechos. Scoreboard puede ser un vector de bits si se desea lograr cobertura de sentencias, un vector de enteros si deseamos conocer la frecuencia de ejecucin de cada sentencia o, en general, una estructura de datos adecuada al criterio de obertura considerado. El siguiente paso es la generacin de la poblacin inicial (conjunto inicial de los valores de entrada). A continuacin (bucle de las lneas 4 a 14), comienza el proceso de generacin de los casos de prueba, bastante legible sin mayor explicacin. Como excepcin, hay que resaltar que el clculo del fitness se hace en funcin del nmero de nodos de CDGPaths alcanzados por el caso de prueba teniendo en cuenta el requisito de prueba actual (variable r en el algoritmo).
16
1. Crear CDGPaths 2. Crear e inicializar Scoreboard 3. Generar CurPopulation 4. while haya requisitos de prueba no marcados and se est en tiempo 5. 6. 7. 8. 9. 10. 11. 12. 13. seleccionar un requisito de prueba r del conjunto TestReq while r no est marcado and no se supere el mximo de intentos calcular el fitness de de CurPopulation usando CDGPaths ordenar CurPopulation segn su fitness seleccionar los padres para crear la NewPopulation generar NewPopulation ejecutar el programa con cada elemento de NewPopulation actualizar Scoreboard y marcar aquellos requisitos que se hayan satisfecho endwhile
14. endwhile 15. final = conjunto de casos de prueba que satisfacen TestReq 16. devolver (TestReq, final) Figura 5. Algoritmo de generacin de casos de prueba de (Pargas, Harrold et al. 1999)
2.03.3 Algoritmos Genticos (II) El trabajo de (Michael, McGaw et al. 2001) es sustancialmente bastante parecido al presentado en la seccin anterior, lo que no es de extraar al estar ambos apoyados en algoritmos genticos: utilizan una poblacin inicial, un conjunto de requisitos de prueba (cobertura de decisiones/condiciones, en este caso), una funcin de fitness, un mtodo de cruce de individuos, etc. En este trabajo, sin embargo, no utilizan grafo de dependencias de control, por lo que el valor del fitness se calcula de una manera ms parecida al trabajo antes mencionado de la Bsqueda Tab (Daz, Tuya et al. 2003).
17
mtodos pblicos de una clase pueden ser invocados. De acuerdo con estos autores, una de las aplicaciones de las secuencias es la prueba de clases a partir de su especificacin como una mquina de estados. As, puede utilizarse la especificacin del comportamiento de una cierta clase en forma de mquina de estados para generar casos de prueba, ejecutarlos y medir nuevos criterios de cobertura. En el ejemplo de la Figura 6, se conseguira una cobertura completa de estados con la secuencia m1.m2.m3.m4, mientras que deberan ejecutarse otras secuencias para lograr cobertura de transiciones o cobertura de caminos. A partir de aqu surgen con facilidad otros criterios de cobertura, como la ejecucin de cada camino cero veces, una vez y ms de una vez, cobertura de guardas, de acciones, etc.
m3
m3 m1 s0 s1 m2 s2 m4 m4
s3
m3
s4
m4
3.01.2 Especificaciones formales o semiformales Lo ms interesante del trabajo de (Tse and Xu 1996) es, por un lado, la explicacin que ofrecen acerca de la especificacin formal de una clase y, por otro, la utilizacin de esta especificacin para obtener el espacio de estados de la clase y generar casos de prueba. De manera general, una clase puede especificarse utilizando combinadamente dos formas de representacin: una capa funcional, en la que se representan los valores abstractos de los objetos de la clase, y una capa de objeto, en la que se anotan la clase y sus operaciones con precondiciones, postcondiciones e invariantes.
18
La Figura 7 muestra las dos capas de una clase Account, que podra representar una cuenta bancaria, con un determinado lenguaje de especificacin (aunque lo cierto es que existen muchos ms para representar lo mismo, como se ver ms adelante en, por ejemplo, la Figura 9).
Capa funcional de una clase module ACCOUNT is including MONEY . sort AccountSort . var X : AccountSort . var M : Money . var N : Money . let creditLimit = 1000 . op empty : -> AccountSort . op creditS : AccountSort Money -> AccountSort . op balance : AccountSort -> Money . eq balance(empty) = 0 . eq balance(creditS(X, M)) = M + balance(X) . op charge : AccountSort Money -> Money . eq charge(empty, N) = 0 . ceq charge(creditS(X, M), N) = N * 0.05 if N < 200 and balance(creditS(X, M)) < 0 . ceq charge(creditS(X, M), N) = 10 if N >= 200 and balance(creditS(X, M)) < 0 . ceq charge(creditS(X, M), N) = 0 if balance(creditS(X, M)) >= 0 . op debitS : AccountSort Money -> AccountSort . eq debitS(empty, N) = creditS(empty, - N) . eq debitS(creditS(X, M), N) = creditS(X, (M - N - charge(creditS(X, M), N))) . endmodule Figura 7. Capas funcional y de objeto de una clase. Figura tomada de (Tse and Xu 1996) Capa de objeto de la misma clase class Account is based on sort AccountSort . invariant {balance(self) >= - creditLimit} . constructor Account() ensure {balance(result) == balance(empty)} . method credit(M : Money) require {M > 0} . ensure {balance(post-self) == balance(creditS(pre-self, M))} . method debit(M : Money) require {M > 0 and balance(debitS(pre-self, M)) >= creditLimit} . ensure {balance(post-self) == balance(debitS(pre-self, M))} . method findBalance() : Money ensure {result == balance(pre-self) and balance(post-self) == balance(pre-self)} . endclass
La capa funcional incluye la descripcin de los tipos importados (including MONEY), el nombre del tipo (sort AccountSort), las posibles variables que se utilicen a continuacin (var X, M, N) y un conjunto de clusulas que especifican los resultados de las operaciones: op representa la signatura de la operacin. Por ejemplo: op charge : AccountSort Money -> Money indica que la operacin charge toma un parmetro de tipo AccountSort y otro de tipo Money y que devuelve un Money.
19
eq representa propiedades de las operaciones descritas mediante ecuaciones. Por ejemplo, eq balance(empty) = 0 indica que el resultado de ejecutar la operacin balance (en ingls, saldo) sobre una cuenta creada con la operacin empty (en ingls, vaca), es cero; eq debitS(empty, N) = creditS(empty, - N) indica que sacar N euros de una cuenta vaca (cuyo saldo es cero) es lo mismo que sacar los mismos N euros a crdito.
ceq representa propiedades de las operaciones descritas mediante ecuaciones condicionales. Por ejemplo, ceq charge(creditS(X, M), N) = N * 0.05 if N < 200 and balance(creditS(X, M)) < 0 indica que sacar
N euros de una cuenta X de la que se han sacado a crdito M euros supone el cobro de un 5% de comisin sobre el importe sacado, si ste es menor que 200 euros y el saldo de la cuenta es negativo. En la capa de objeto del caso de la figura incluye, por ejemplo, una invariante que indica que el saldo de la cuenta debe ser superar siempre al crdito concedido (invariant {balance(self) >= - creditLimit}); una postcondicin sobre el constructor Account que denota que el saldo de una cuenta recin creada es cero (constructor Account() ensure {balance(result) == balance(empty)}), y una precondicin para la operacin credit(M : Money) que expresa que el importe que se saca a crdito debe ser positivo (require { M>0 }). A partir de estas consideraciones, los autores realizan una particin del espacio de estados de la clase, obteniendo estados abstractos por cada trmino booleano de la capa funcional. A partir, por ejemplo, de la propiedad eq balance(empty)=0 podran obtenerse los estados en que la cuenta tiene saldo negativo, saldo cero y saldo positivo. Combinando adems estos estados con la invariante de la capa de objeto (invariant {balance(self) >= - creditLimit}), se podran obtener los siguientes cinco subestados: balance(self) < -creditLimit balance(self) = -creditLimit 0> balance(self) > -creditLimit balance(self) = 0 balance(self) > 0 inalcanzable debido a la invariante
20
Puesto que el valor de creditLimit es 1000, los estados concretos son los siguientes (ntese la adicin de un estado inicial adicional, en el que el objeto se encuentra antes de ser creado; igualmente podran crearse estados finales si la clase tuviera destructores): S0= { no creado }; S1= { b==1000 }; S2= { -1000<b<0}; S3= { b==0}; S4={ b>0} Obtenidos los estados, se genera el conjunto de transiciones, formadas por una llamada a un mtodo y una posible condicin de guarda, obtenida de las precondiciones del mtodo. De este modo, del anterior conjunto de estados y del ejemplo de la Figura 7 se obtiene la mquina de estados mostrada en la Figura 8.
t0= Account() t1= credit(m) {0 < m < 1000} t2= credit(m) {m == 1000} t3= credit(m) {1000 < m} t4 = credit(m) {0 < m < 1000 and b + m == 0} t5 = credit(m) {(0 < m < 1000 and 0 < b + m) or 1000 = m} t6 = credit(m) {0 < m < 1000 and -1000 < b + m < 0} t7 = credit(m) {0 < m} t8 = credit(m) {0 < m} t9 = debit(m) {1000 < m and b - m == -1000} t10 = debit(m) {0 < m and -1000 < b - m < 0} t11 = debit(m) {0 < m and b - m == 0} t12 = debit(m) {0 < m and 0 < b - m} t13 = debit(m) {0 < m < 1000} t14 = debit(m) {m == 1000} t15 = debit(m) {(200 m < 990 and b - m == -990) or (0 < m < 200 and b - 1.05 * m == -1000)} t16 = debit(m) {(200 =m < 990 and -990 < b - m < 10) or (0 < m < 200 and -1000 < b - 1.05 * m < 0)} t17 = findBalance() t18 = findBalance() t19 = findBalance() t20 = findBalance()
La mquina de estaos se procesa para generar secuencias de mtodos que logren diferentes criterios de cobertura, como cobertura de estados, transiciones y caminos. 3.01.3 Mtodo ASTOOT (Doong and Frankl 1994) utilizan especificaciones algebraicas de las clases para la generacin y ejecucin de casos de prueba en su mtodo ASTOOT (A Set of Tools for Object-Oriented Testing). Una especificacin algebraica consta de una parte sintctica y otra semntica: La sintctica incluye los nombres y signatura de las operaciones.
21
La semntica incluye una lista de axiomas que describen la relacin entre funciones, utilizndose en muchos casos axiomas de reescritura para esta descripcin. As, dos secuencias de operaciones S1 y S2 son equivalentes si se pueden usar los axiomas como reglas de reescritura para transformar S1 en S2.
La siguiente figura, tomada de (Doong and Frankl 1994), muestra la especificacin algebraica de una cola de prioridad. De acuerdo con esta figura, la secuencia create.add(5).delete(3) es equivalente a la secuencia create.add(5), pues puede aplicarse dos veces el sexto axioma.
Figura 9. Especificacin algebraica de una cola de prioridad, tomada de (Doong and Frankl 1994)
Para estos autores, dos objetos O1 y O2 de clase C son observacionalmente equivalentes si y slo si:
22
(1)
C es una clase primitiva (entero, real...), O1 y O2 tienen valores idnticos. C no es una clase primitiva y, para cualquier secuencia S de operaciones de C que devuelven un objeto de clase C, O1.S es observacionalmente equivalente a O2.S como objeto de clase C.
(2)
En otras palabras, cuando es imposible distinguir O1 de O2 usando las operaciones de C. Si se dispusiera de una cantidad de tiempo infinita para comprobar si dos objetos son observacionalmente equivalentes, podra utilizarse el siguiente procedimiento para comprobar la correccin de la clase C: Sea U el conjunto de tuplas (S1, S2, etiqueta), donde S1 y S2 son secuencias de mensajes, y etiqueta es un texto que vale equivalente o no equivalente. Para cada tupla de U, enviar las secuencias S1 y S2 a O1 y O2 y comprobar si ambos objetos son observacionalmente equivalentes. Si todas las equivalencias observacionales se comprueban de acuerdo con las etiquetas, entonces la implementacin de C es correcta, e incorrecta en caso contrario. La equivalencia observacional de dos objetos puede resolverse con un mtodo tipo equals; sin embargo, la finitud del tiempo para realizar las pruebas es irresoluble, por lo que sus autores la han adaptado en la familia de herramientas ASTOOT. El componente de generacin de casos de prueba toma la descripcin sintctica y semntica de la clase que se va a probar (Figura 9) y la traduce a una representacin arborescente. Con esta representacin, lee una secuencia de operaciones suministrada por el usuario y le aplica una serie de transformaciones para obtener secuencias de operaciones equivalentes. Las operaciones incluidas en las secuencias son simblicas, en el sentido de que carecen de parmetros reales. Por ltimo, y teniendo en cuenta la asuncin explicada ms arriba, la comprobacin de la correccin de la clase la realizan pasando valores reales a los parmetros de los mensajes contenidos en las secuencias de cada tupla.
23
3.01.4 Obtencin automtica de especificaciones algebraicas (Henkel and Diwan 2003) han desarrollado una herramienta que obtiene de manera automtica especificaciones algebraicas a partir de clases Java. Comienzan obteniendo una lista de secuencias vlidas mediante llamadas sucesivas a los constructores de la clase y a algunos de sus mtodos, pasando valores adecuados a los parmetros de estas operaciones. Si alguna de las secuencias lanza una excepcin, se deshace la ltima operacin o se prueba con un nuevo valor del argumento (Figura 10).
Figura 10. Generacin incremental de trminos aplicada a la clase IntStack (Pila de enteros), tomada de (Henkel and Diwan 2003)
Generadas las secuencias, obtienen ecuaciones de igualdad del estilo de las mostradas en la Figura 7 y en la Figura 9, pero que incluyen valores reales en lugar de simblicos: Ecuaciones de igualdad de secuencias diferentes, como por ejemplo:
pop(push(IntStack().state, 4).state).state=IntStack().state
A partir de las ecuaciones se generan los axiomas, que constan de dos secuencias y una serie de variables cuantificadas. As, de la ecuacin de la izquierda, se obtiene el axioma de la derecha:
IntAdd(size(IntStack().state).retval,1).retval = size(push(IntStack().state, 3).state).retval s:IntStack, i:int IntAdd(size(s).retval,1).retval = size(push(s, I).state).retval
24
Muchos de los axiomas generados son redundantes, por lo que con posterioridad se detectan y se eliminan aplicando reglas de reescritura, para lo que primero deben identificarse stas de entre el conjunto de axiomas. Se considera que un axioma es una regla de reescritura si: (1) el lado izquierdo y el derecho tienen diferente longitud; y (2) las variables libres que aparecen en lado ms corto son un subconjunto de las variables libres que aparecen en el lado derecho.
25
cial porque tal vez no llegue fsicamente a producirse en la ejecucin del sistema]. Interaccin entre objetos (interaccin real): siendo A, B dos clases, ocurre si y slo si (1) A Ri B y B Ri A, siendo i distinto de j, y (2) Ri y Rj son relaciones transitivas reales. [La interaccin es real porque se produce la interaccin entre ambos objetos en la ejecucin del sistema obtenido del diseo]. El criterio de prueba propuesto es el siguiente: para cada interaccin entre clases, o bien se obtiene un caso que prueba la interaccin entre objetos, o bien se obtiene un informe mostrando que tal interaccin no es factible. Puesto que la tarea de producir casos de prueba o informes es imposible si el nmero de interacciones es muy alto, es preciso obtener diseos que disminuyan el nmero de interacciones, de modo que se haga factible as la prueba completa del diseo de clases. Antes de someterlo al criterio de pruebas, el diseo de clases debe ser evaluado para conocer el nmero de interacciones entre clases, modificndolo si es muy alto y tal mejora es posible, o rechazndolo directamente en otro caso. La mejora del diseo puede lograrse reduciendo el acoplamiento o anotndolo con restricciones que eviten la codificacin de interacciones entre objetos propensas a error. Los autores proponen la anotacin utilizando los estereotipos <<create>> (para indicar que la clase A crea instancias de la clase B), <<use_consult>> (para indicar que la clase A slo utiliza mtodos tipo get de B) y <<use_def>> (para indicar que la clase A modifica el estado de las instancias de B). Estos estereotipos se utilizan en el proceso de construccin del Grafo de Dependencias de Clases (en ingls Class Dependency Graph, o CDG), que representa las relaciones transitivas entre clases junto a las relaciones de herencia e implementacin. La siguiente figura muestra algunos ejemplos sobre la obtencin del CDG, en la que aparecen algunas etiquetas aadidas a los nodos del CDG para representar el tipo de relacin existente.
26
Figura 12. Algunos ejemplos de obtencin del CDG, tomada de (Baudry, Traon et al. 2002)
A partir del CDG pueden calcularse medidas como la complejidad de una interaccin, definida en funcin de la complejidad de los diferentes caminos por los que puede irse desde un objeto hasta otro:
complejidad (CI ) =
nbPaths i =1
((complejidad ( P ) complejidad ( P ))
i j >i j
La complejidad de un camino en una interaccin es necesaria para calcular el valor de la Ecuacin 1, y se define como el productorio de la complejidad asociada a cada jerarqua cruzada por la interaccin (parmetro IH):
complejidad ( P) =
nbCrossed i =1
complejidad ( IH , P)
Obviamente, es preciso poder calcular la complejidad de un camino que pasa por una jerarqua de herencia:
complejidad ( IH , P) =
nbDP i =1
complejidad (dp )
i
Por ltimo, la complejidad de un camino de descendientes es el nmero de interacciones potenciales entre las clases que hay en ese camino. El caso peor se dara cuando todas las clases estuvieran relacionadas con todas las clases, lo que supone un valor mximo de n(n-1). De manera general, la com-
27
plejidad de un camino se corresponde con la Ecuacin 4, en donde h representa la altura del camino:
comlejidad (dp ) = h (h 1)
Ecuacin 4. Complejidad de un camino de descendientes
En la siguiente figura, tomada de (Baudry, Traon et al. 2002), la complejidad del camino que va desde client hasta b1 pasando por la relacin de herencia de la clase d es 1+3*(3-1)+1: 1 por la relacin de uso de client con respecto a d; 3*(3-1) por el camino de descendientes de la jerarqua d, y otra vez 1 por la relacin de uso desde d22 a b1.
3.02.2 Criterios de cobertura de pruebas para diseos UML (Andrews, France et al. 2003) proponen varios criterios de cobertura para las pruebas de diferentes diagramas UML. Para diagramas de clases, proponen los siguientes: AEM (Association-end multiplicity): dado un conjunto de pruebas T y un modelo SM, T debe causar que se cree cada par de multiplicidades representativo en las asociaciones de SM. As, si existe una asociacin cuya multiplicidad es, en un extremo, p..n, debera instanciarse la asociacin con p elementos (valor mnimo), n elementos (valor mximo) y con uno o ms valores en el intervalo (p+1, n-1). GN (Generalization): dado un conjunto de pruebas T y un modelo SM, T debe conseguir que se cree cada relacin de generalizacin de SM. CA (Class attribute): dado un conjunto de pruebas T, un modelo SM y una clase C, T debe conseguir que se creen conjuntos de valores representativos de los diferentes atributos de la clase C. El conjunto de
28
valores representativos se consigue en tres pasos: (1) crear valores representativos para cada atributo, para lo que pueden usarse clases de equivalencia; (2) calcular el producto cartesiano de estos valores; (3) eliminar los conjuntos de valores invlidos, considerando el dominio del problema, posibles restricciones que anoten el diagrama, etc. Para diagramas de interaccin, los criterios propuestos son: Cobertura de condiciones: dado un conjunto de casos de prueba T y un diagrama de interaccin D, T debe conseguir que cada condicin del diagrama se evale a true y a false. Cobertura completa de predicados (FP: full predicate coverage): cada clusula de cada condicin debe evaluarse a true y a false. Cobertura de mensajes (EML: each message on link): cada mensaje del diagrama debe ejecutarse al menos una vez. Cobertura de caminos (AMP: all message paths): todos los posibles caminos de ejecucin deben ejecutarse. Cobertura de colecciones (Coll: Collection coverage): el conjunto de casos de prueba debe probar cada interaccin con colecciones al menos una vez. 3.02.3 Diagramas de estados (Burton, Clark et al. 2001; Hong, Lee et al. 2001) 3.02.4 En otros diagramas (Basanieri, Bertolino et al. 2002) utilizan diagramas de casos de uso y de secuencia para derivar casos de prueba desde las etapas iniciales del desarrollo de un sistema orientado a objetos. Tras realizar una serie de anlisis y adaptaciones de los diagramas, aplican los siguientes pasos: Definir el conjunto de secuencias de mensajes MS a partir de los diagramas de secuencia. Cada secuencia comienza con un mensaje m sin predecesor (habitualmente, un mensaje enviado al sistema por un actor) y el conjunto de mensajes cuya ejecucin dispara m (aquellos cuyo inicio est en el foco de control en que termina m).
29
Analizar de subcasos, que bsicamente consiste en construir varias secuencias a partir de las posibles instrucciones condicionales que anotan los diagramas de secuencia.
Identificar de los conjuntos de valores de prueba, que se construyen a partir de los tipos de los parmetros de los mtodos y del anlisis de los diagramas de clases del sistema.
Seleccionar, para cada mensaje de la secuencia, las situaciones relevantes en que el mensaje puede ocurrir y, para cada valor de prueba, valores vlidos que puedan incluirse en el mensaje.
Eliminar valores contradictorios o poco significativos, para lo que los autores sugieren utilizar clases de equivalencia.
Obtener procedimientos de prueba, que puede hacerse automticamente con los datos obtenidos en los pasos anteriores.
As, a partir del diagrama de secuencia mostrado en la Figura 13, se identifican las siguientes secuencias de mensajes:
MS_1: 1.start(), 1.1.open() MS_2: 2.enterUserName(String) MS_3: 3.enterPassword(String) MS_4: 4.loginUser(), 4.1.validateuserIDPassword(String, String) 4.2.setupSecurityContext(), 4.2.1.new UserID() 4.3.closeLoginSelection()
30
Figura 13. Diagrama de secuencia de ejemplo, tomado de (Basanieri, Bertolino et al. 2002)
El diagrama incluye sin embargo una instruccin condicional, por lo que la secuencia MS_4 puede dividirse en dos:
MS_4.1: 4.loginUser(), 4.1.validateuserIDPassword(String, String) 4.2.setupSecurityContext(), 4.2.1.new UserID() MS_4.2: 4.loginUser(), 4.1.validateuserIDPassword(String, String) 4.3.closeLoginSelection()
31
del componente, se ejecutan las operaciones de ste a travs del BIT wrapper, que posee funcionalidades como la comprobacin de las precondiciones de la operacin antes de llamar a la operacin real, y la comprobacin de las postcondiciones tras su ejecucin. Adems, el BIT wrapper puede mantenerse para que capture las llamadas que los clientes hacen a las operaciones del componente (Figura 14).
Figura 14. Ubicacin del BIT wrapper alrededor del componente. Figura tomada de (Edwards 2001)
Algunas caractersticas de los BIT wrappers son las siguientes: Son transparentes al componente y a los posibles clientes. La adicin o supresin de BIT wrappers slo requiere la modificacin de las declaraciones en el cdigo de los clientes. Se aaden nuevas capacidades de comprobacin de restricciones a las que ya realiza el propio componente. La violacin de las restricciones se detectan en el momento en que ocurren, de manera que se evita su propagacin a otros componentes. Si se posee una especificacin formal del componente, el BIT wrapper se puede generar automticamente.
32
Para la fase de pruebas del componente, (Edwards 2001) propone el proceso que se muestra esquemticamente en la Figura 15: el conjunto de casos de prueba contenido en el Test suite (que se ha podido generar automticamente) se pasa a un ejecutor de pruebas (Test driver), que prueba el componente a travs del BIT wrapper. El resultado es, por un lado, los resultados obtenidos de la ejecucin de cada caso de prueba y, por otro, un informe con los errores encontrados.
Tanto el BIT wrapper como el Test driver pueden generarse automticamente: para el primero es preciso disponer de una descripcin formal del componente (el autor utiliza el lenguaje Resolve, aunque podra emplearse cualquier otro); para la generacin del segundo podran usarse generadores aleatorios, de valores lmite, etc. 3.03.2 Mutacin de interfaces (Ghosh and Mathur 2001) proponen aplicar ciertos operadores de mutacin a las interfaces de componentes para la realizacin de pruebas, as como ciertos criterios de cobertura para validar las pruebas realizadas. Los autores proponen los siguientes operadores de mutacin para CORBAIDL son los siguientes: 33
Reemplazar inout por out. Reemplazar out por inout. Intercambiar parmetros de tipos compatibles. Jugar con un parmetro (operador twiddle): por ejemplo, sumarle uno si es entero, aadirle un carcter si es una cadena, etc.
Los criterios de cobertura que proponen son los siguientes: Cobertura de llamadas a mtodos de la interfaz del componente. Cobertura de excepciones lanzadas por la interfaz del componente. Cobertura de llamadas a mtodos y de excepciones lanzadas.
La mutacin se consigue sustituyendo la interfaz por una nueva versin, como muestra esta figura:
Cliente Interfaz
Servidor
Cliente
Interfaz mutada
Servidor
4. RESULTADOS EXPERIMENTALES
(Juristo, Moreno et al. 2002) han analizado los resultados experimentales de diferentes tcnicas de prueba extradas de la literatura, de acuerdo con la siguiente clasificacin: Familia de tcnicas aleatorias, en las que los casos de prueba son generados aleatoriamente, sin seguir ninguna pauta preestablecida. Familia de tcnicas funcionales, que utilizan la especificacin del programa para generar casos de prueba de caja negra. Familia de tcnicas de flujo de control, que requieren el conocimiento del cdigo fuente para seleccionar una serie de caminos a lo largo del programa, de modo que se ejecute su modelo de control. 34
Familia de tcnicas de flujo de datos, que requieren tambin el conocimiento del cdigo fuente para seleccionar secuencias de eventos relacionados con el estado de los datos.
Familia de tcnicas de mutacin, que modelan mediante operadores de mutacin los errores tpicos que se cometen al hacer un programa, y lo ejecutan mediante casos de prueba que detecten dichos errores.
Los resultados de cada familia los ubican en una de las tres siguientes categoras: (1) Nivel 1: afirmaciones no contrastadas de manera empricamente formal. (2) Nivel 2: afirmaciones contrastadas empricamente pero no con situaciones reales; es decir, con programas no reales o con fallos lo reales. (3) Nivel 3: afirmaciones contrastadas empricamente en situaciones reales. Respecto de la Familia de tcnicas de flujo de datos, las autoras analizan dos trabajos que sitan a esta familia en los niveles 2 y 3. Respecto de la Familia de tcnicas de mutacin, se analizan tres trabajos que sitan a esta familia en nivel 2. Igualmente, realizan un estudio comparativo entre las familias de flujo de datos, flujo de control y aleatoria. Las autoras afirman que, ante restricciones de tiempo se pueden usar tcnicas aleatorias confiando en que en el 50% de los casos nos dar una efectividad similar a la de all-uses y all-edges [...]. Si se necesita un testing exhaustivo, entonces se puede asegurar aplicando alluses. No obstante, esta afirmacin la sustentan a nivel 2 y no a nivel 3. Tambin comparan la familia de mutacin y la de flujo de datos, indicando que si se busca conseguir cobertura alta y no se dispone de mucho tiempo, es preferible usar all-uses frente a mutacin, ya que en aproximadamente la mitad de los casos ser igual de efectiva que la mutacin, sustentando esta afirmacin en un nivel 2. Comparan tambin la familia funcional con la de flujo de control, lo que viene a significar una comparacin entre pruebas de caja negra y de caja blanca. Afirman que, si se est trabajando con sujetos con experiencia, en caso de disponer de tiempo suficiente, es mejor utilizar la tcnica de anlisis de valores lmite frente a cobertura de sentencias ya que se encontrarn ms fallos, aun-
35
que les llevar ms tiempo. Por el contrario, si se est trabajando con sujetos sin experiencia y no se tiene tiempo, es mejor usar cobertura de sentencias. Otras conclusiones son que es preferible usar anlisis de valores lmite frente a cobertura de condicin. Todas estas conclusiones se encuentran a nivel 2. De manera general, las autoras destacan la ubicacin en nivel 2 de la mayora de las tcnicas, lo que indica que hay an mucha investigacin que realizar.
5. REFERENCIAS
Andrews, A., R. France, et al. (2003). "Test adequacy criteria for UML design models." Software Testing, Verification and Reliability(13): 95-127. Basanieri, F., A. Bertolino, et al. (2002). The Cow_Suite Approach to Planning and Deriving Test Suites in UML Projects. 5th International Conference on The Unified Modeling Language, Springer-Verlag. LNCS. Bashir, I. and A. L. Goel (2000). Testing Object-Oriented Software. Life Cycle Solutions. New-York, Springer-Verlag. Baudry, B., Y. L. Traon, et al. (2002). Testability Analysis of a UML Class Diagram. 8th IEEE Symposium on Software Metrics. Burton, S., J. Clark, et al. (2001). Automatic generation of tests from statecharts specifications. Formal Approaches to Testing of Software, Aalborg, Denmark, BRICS. Cornett, S. (2002). Code Coverage Analysis. Daz, E., J. Tuya, et al. (2003). Pruebas automticas de cobertura de software mediante una herramienta basada en Bsqueda Tab. VIII Jornadas de Ingeniera del Software y Bases de Datos, Alicante, Spain. Doong, R. K. and P. G. Frankl (1994). "The ASTOOT approach to testing object-oriented programs." ACM Transactions on Software Engineering and Methodology 3(2): 101-130. Edwards, S. H. (2001). "A framework for practical, automated black-box testing of component-based software." Software Testing, Verification and Reliability(11): 97-111. Edwards, S. H. (2001). "A framework for practiucal, automated black-box testing of component-based software." Software Testing, Verification and Reliability(11): 97-111. Fox, C. (1998). Java Code Inspection Checklist. Ghosh, S. and A. P. Mathur (2001). "Interface mutation." Software Testing, Verification and Reliability(11): 227-247. Henkel, J. and A. Diwan (2003). Discovering Algebraic Specifications from Java Classes. 17th European Conference on Object-Oriented Programming (ECOOP), Springer. Hong, H. S., I. Lee, et al. (2001). Automatic test generation from statecharts using model checking. Formal Approaches to Testing of Software, Aalborg, Denmark, BRICS.
36
ISO/IEC (1995). ISO/IEC 12207. International Standard. Software Life Cycle Processes. Geneve, International Standard Organziation/International Electrotechnical Committee. Juristo, N., A. M. Moreno, et al. (2002). A Survey on Testing Technique Empirical Studies: How Limited is our Knowledge. International Symposium on Empirical Software Engineering (ISESE'02), Nara, Japan. Kim, S., J. A. Clark, et al. (2000). Class Mutation: Mutation Testing for ObjectOriented Programs. International Conference on Object-Oriented and Internet-based Technologies, Concepts, and Applications for a Networked World, Net.ObjectDays'2000, Germany. Kirani, S. and W. T. Tsai (1994). "Method sequence specification and verification of classes." Journal of Object-Oriented Programming 7(6): 28-38. Lapierre, S., E. Merlo, et al. (1999). Automatic Unit Test Data Generation Using Mixed-Integer Linear Programming and Execution Trees. International Conference on Software Maintenance, Oxford, England. Michael, McGaw, et al. (2001). "Generating Software Test Data by Evolution." IEEE Transactions on Software Engineering 27(12): 1085-1110. Offut, A. J. (1995). A practical system for mutation testing: help for the common programmer. 12th International Conference on Testing Computer Software. Offut, A. J., G. Rothermel, et al. (1996). "An experimental determination of sufficient mutant operators." ACM Transactions on Software Engineering and Methodology 5(2): 99-118. Pargas, R. P., M. J. Harrold, et al. (1999). "Test-Data Generation Using Genetic Algorithms." Software Testing, Verification and Reliability(9): 263-282. Rice, R. W. (2002). "Surviving the top 10 challenges of software test automation." CrossTalk: The Journal of Defense Software Engineering(Mayo): 26-29. Tracey, N., J. Clark, et al. (1998). Automated program flaw finding using simulated annealing. International Symposium on Software Testing and Analysis, Clearwater Beach, Florida, USA, ACM/SIGSOFT. Tse, T. and Z. Xu (1996). Test Case Generation for Class-Level ObjectOriented Testing. 9th International Software Quality Week, San Francisco, CA.
37