
Tema de la Clase del Día 09/03/1999
Tópico: Testing Orientado por Objetos
Isabel Lameda, Carnet 96-28590
Eliécer Peraza, Carnet 96-28810
[Pruebas de Métodos] [Evaluación de un Método de Generación de Prueba] [Método Caja Negra] [Método Híbrido de Generación de Pruebas] [Pruebas Unitarias] [Enlaces] [Bibliografía]
1 Pruebas de métodos
Cada método de una clase puede ser sometido a pruebas para determinar si está correctamente implementado de acuerdo con la especificación que contiene el modelo funcional.
Algunos métodos son lo suficientemente sencillos como para no ameritar muchas pruebas.
En general, las estrategias de generación de casos de prueba se dividen en:
1. Métodos de caja blanca;
2. Métodos de caja negra.
En los métodos de caja blanca determinamos cuáles son los casos de prueba a partir del código fuente del software y se utilizan las especificaciones para determinar el resultado esperado del caso.
En los métodos de caja negra, los casos de
prueba y los resultados se determinan a partir de la
especificación funcional del método de una clase.
1.1 El método de cobertura ciclomática de caminos
Fue propuesto por McCabe.
Se basa en construir un caso de prueba por camino linealmente independiente (también llamado común o básico) que se encuentre en el grafo de programa asociado al método de la clase que se desea someter a pruebas.
Los pasos del método son:
1. Dibujar G, el grafo del programa del método de la clase;
2. Determinar el número ciclomático del grafo, V(G).
V(G) = E - N + 2, donde E, es el número de aristas del grafo y N el número de nodos.
Cuando G es un grafo planar, este número corresponde al número de regiones en el grafo.
Equivalentemente se puede calcular V(G) tomando en número total de nodos condición C más una unidad, es decir:
V(G) = C + 1
3. Construir una secuencia de V(G) caminos linealmente independientes en G. El primer camino es:
Cualquiera de los caminos mínimos del nodo origen del grafo a uno de los nodos de terminación del grafo. Se inicializa A(G), el conjunto de aristas en la secuencia linealmente independiente de G con todas aquellas aristas que forman este primer camino.
Los demás caminos se construyen siguiendo la siguiente regla: Agregar un nuevo camino entre el origen y una terminación de G. Este nuevo camino debe agregar el mínimo número posible de aristas nuevas a A, (pero debe agregar por lo menos una arista nueva).
4. Preparar un caso de prueba por camino hallado en el paso anterior.
(a) Determinar los datos a proporcionar como entrada para ejecutar el camino hallado.
(b) Usando la especificación funcional del método, indicar cuál es el resultado esperado.
A continuación se presenta un ejemplo en el cual se ilustran todos los pasos a seguir para generar casos de prueba con el método de cobertura ciclomática de caminos
Ejemplo: Método del promedio filtrado
Este método calcula el promedio de
algunos elementos de un arreglo "v" de números
enteros. Los elementos a los cuales se les obtendrá el promedio
se encuentran acotados por un número máximo (max) y un número
mínimo (min); es decir dentro de todos los elementos que posee
el arreglo sólo es posible tomar aquellos comprendidos entre los
valores min y max. Además los elementos que se escogen del
arreglo también están acotados por una posición (num), es
decir, los posibles elementos escogibles para la operación se
encuentran entre la posición 0 y la posición num del arreglo.
En resumen, un elemento "x" formará parte del conjunto
de elementos a los cuales se les obtendrá el promedio si cumple
con cada una de las siguientes condiciones:
- Encontrarse entre la posición 0 y la posición num del arreglo "v" (si x = v[i], entonces 0 £ i < num)
- Ser a la vez menor o igual a max y mayor o igual a min ( min £ x £ max )
Por ejemplo si se tiene
v = [2, 6, 8, 10, 5, 9]
min = 3
max = 10
num = 4,
el resultado del promedio
filtrado sería :
(6 + 8 + 10)/3 = 24 / 3 = 8
claramente 6, 8 y 10 son mayores o iguales a min, son menores o iguales a max y sus posiciones son menores a 4 ( son 1, 2 y 3 respectivamente).
Código fuente del método del promedio filtrado:
class promedio
{...
private int[] v;
private int min,max;
public double promFiltrado(int num)
trows DividedByZero{
int
i=0;
int valid=0;
int sum=0;
while
(i<v.length() && i<num){
if (v[i]>=min && v[i]<=max){
valid++;
sum+=v[i]
}
i++;
}
if (valid <>
0)
return (sum/valid);
else
throw DivideByZero();
}
...
}
1) Grafo del Programa "promFiltrado" ( Método del promedio filtrado)

Para realizar el grafo se agrupan las acciones
independientes en un nodo (o bloque), como los nodos A y F del
ejemplo (cabe destacar que en el grafo los nodos están
identificados por letras mayúsculas de color azul).
Se separan todas y cada una de las
condiciones (formando un nodo por cada una de ellas) y se
toman en cuenta todos los posibles resultados que se puedan
obtener al evaluarlas. Es decir, es creado un nuevo nodo,
secuencialmente por cada condición que posea el programa:
En el ejemplo la condición (i<v.length
&& i<num)se separa en evaluar primero i<v.length, y dependiendo del
resultado de esta evaluación se podría considerar o no la
siguiente condición i<num.
2) Número ciclomático del grafo
Número de
aristas del grafo : E = 15
Número de nodos del
grafo : V = 11
Número de nodos
condición : C = 5
V(G)
: E - V + 2 = 15 - 11 + 2 = 6
V(G) : número de
regiones = 6 (las regiones están enumeradas en el grafo en color
rojo. Esta manera de determinar el número ciclomático sólo es
válida para grafos planares)
V(G) = C + 1 = 5 + 1 =
6
3) Secuencia de caminos linealmente independientes en el grafo (para el caso en cuestión, son 6 los caminos para la secuencia a construir ya que V(G)=6).
El primer camino es un camino mínimo del nodo
inicial al uno de los finales del grafo, en el ejemplo se tiene
un solo nodo final. Por lo tanto este camino sería comenzar por
el nodo A, luego el B, el H y finalmente el J.
Para generar el segundo camino se recorrería
el grafo igual que el caso anterior pero terminando en el nodo I.
El tercer camino independiente sería comenzar
por el nodo A, seguido de el B,C,H,I. Nótese que se pudo haber
escogido el camino A,B,C,H,I porque las aristas H,I y H,J ya
fueron recorridas en caminos anteriores
Cuarto camino A,B,C,D,G,B,H,J. Al igual que en
el caso anterior, es equivalente tomar el camino A,B,C,D,G,B,H,I.
Y aplicando este procedimiento se obtienen
los siguientes caminos linealmente independientes:
1) A,B,H,J
2) A,B,H,I
3) A,B,C,H,J
4) A,B,C,D,G,B,H,J
5) A,B,C,D,E,G,B,H,J
6) A,B,C,D,E,F,G,B,H,I
Nótese que esta secuencia no es única; es decir es posible obtener otras secuencias de caminos linealmente independientes, como por ejemplo:
1) A,B,H,J
2) A,B,H,I
3) A,B,C,H,I
4) A,B,C,D,G,B,H,I
5) A,B,C,D,E,G,B,H,I
6) A,B,C,D,E,F,G,B,H,J
Sin embargo, algunos de estos caminos no son factibles. Tal es el caso del camino 3, entre otros, lo cual se explicará más adelante.
4) Casos de prueba para cada camino hallado
1) Camino ABHJ
Inicialmente i=0 y como para ejercitar este camino es necesario (i < v.length) = false, se tiene que crear un objeto promedio con arreglo nulo (de tamaño 0). Además, valid =0 por lo que luego se pasaría al nodo J. Los valores de min, max, num son irrelevantes para este caso de prueba, por lo que pueden tomar cualquier valor sin afectar la ejecución del método para este caso en particular.
Caso específico : v[]=null, por lo que v.length( ) es igual a 0.
max= 3,
min = 2,
num=2,
Resultado esperado: excepción (DividedByZero).
Recorrería el grafo de la siguiente manera (pasando por las aristas que se muestran en el siguiente subgrafo):
Subgrafo del Programa "promFiltrado" ( Método del promedio filtrado) que representa el camino ABHJ

2) Camino ABHI
Para este caso también se necesita i < v.length, por lo que es necesario crear un objeto promedio con arreglo nulo. Pero, en cualquier caso valid = 0 así que no se estaría recorriendo este camino. Por lo tanto, el camino A,B,H,I es un camino no factible. Así que es necesario verificar sí la arista H, I puede generarse en algún otro camino.
Recorrería el grafo de la siguiente manera (pasando por las aristas que se muestran en el siguiente subgrafo):
Subgrafo del Programa "promFiltrado" ( Método del promedio filtrado) que representa el camino ABHI

3) Camino ABCHJ
En este caso sí se necesita i < v.length, luego se debe crear un objeto promedio con arreglo de tamaño 1,2,3,... Parece ser indiferente si se escoge cualquiera de estos tamaños. Ahora, se necesita (i<num) = false por lo que num debe ser igual a 0, ya que i = 0 y la salida esperada sería una excepción por división por cero. Nótese que como valid no es mayor que 0, el camino A,B,C,H,I ( que es equivalente al A,B,C,H,J) no es factible.
Caso específico: v[i] = [6],
min= 3,
max= 4,
num = 0,
Resultado esperado: excepción (DividedByZero).
Recorrería el grafo de la siguiente manera (pasando por las aristas que se muestran en el siguiente subgrafo):
Subgrafo del Programa "promFiltrado" ( Método del promedio filtrado) que representa el camino ABCHJ

4) Camino ABCDGBHJ
Para recorrer este camino se necesita, al igual que en el caso anterior, i < v.length por lo que de nuevo el tamaño del arreglo debe ser mayor que 0. También se necesita i<num y como i = 0, num debe ser cualquier valor mayor que 0. Falta cumplir con la condición (v[i] >= min) = false y sólo basta con escoger valores adecuados para v[i] y min. Ahora, i=1 y se necesita (i < v.length) = false, luego el tamaño del arreglo debe ser igual a 1 y no cualquier número mayor que 0. Luego (valid > 0) = false y la salida esperada sería nuevamente una excepción.
Caso específico: v[i] = [1],
min= 3,
max= 4,
num = 1,
Resultado esperado: excepción (DividedByZero).
Recorrería el grafo de la siguiente manera (pasando por las aristas que se muestran en el siguiente subgrafo):
Subgrafo del Programa "promFiltrado" ( Método del promedio filtrado) que representa el camino ABCDGBHJ

5) Camino ABCDEGBHJ
Para recorrer este camino es necesario crear un objeto promedio con arreglo de tamaño 1 por lo que se explicó en el caso anterior. Además num debe ser mayor que cero y se debe escoger v[i] >= min y v[i] > max, en cuyo caso se esperarían una excepción por división por 0.
Caso específico: v[i] = [6],
min= 3,
max= 4,
num = 1,
Resultado esperado: excepción (DividedByZero).
Recorrería el grafo de la siguiente manera (pasando por las aristas que se muestran en el siguiente subgrafo):
Subgrafo del Programa "promFiltrado" ( Método del promedio filtrado) que representa el camino ABCDEGBHJ

6) Camino ABCDEFGBHI
En este caso también se necesita que el arreglo sea de tamaño 1 con num>0, v[i]>=min y v[i]<=max , en este caso se pasaría al nodo F donde valid = 1 y sum = v[i], luego en el nodo G se tiene que i=1 por lo que (i<v.length) = false, pero ahora valid >0 por lo que la salida esperada sería sum. En este caso se estaría recorriendo la arista H,I que no era factible anteriormente.
Caso específico: v[i] = [3],
min= 2,
max= 5,
num = 1,
Resultado esperado: 3.
Recorrería el grafo de la siguiente manera (pasando por las aristas que se muestran en el siguiente subgrafo):
Subgrafo del Programa "promFiltrado" ( Método del promedio filtrado) que representa el camino ABCDEFGBHI

Nótese que ninguno de los casos de prueba permite que el ciclo (while) se ejecute más de una vez, por lo que el método ciclomático de generación de caminos no estaría detectando errores que posiblemente se produzcan cuando se ejecute varias veces dicho ciclo. Por lo tanto, se puede afirmar que este método de generación de caminos no es bueno para detectar errores que se manifiestan cuando se realizan iteraciones, este tipo de errores se llaman errores ocultos y serán explicados más adelante.
2 Evaluación de un Método de Generación de
Prueba
Para evaluar un método de generación de pruebas tenemos que responder dos preguntas:
2.1 Efectividad
El método de generación de pruebas por Cobertura Ciclomática de Caminos (Método CCC de ahora en adelante) se fundamento en dos suposiciones:
1. Los defectos están homogéneamente distribuidos entre los caminos linealmente independientes del grafo del programa.
2. Cualquier dato que ejercite un camino tiene la misma probabilidad (alta) de detectar el potencial defecto que contiene.
Son ciertas estas suposiciones?
2.1.1 Los defectos no están uniformemente distribuidos.
Los únicos defectos que están distribuidos uniformemente en un programa son los errores tipográficos. De hecho se suele esgrimir como justificación de los métodos de caja blanca que son efectivos para hallar errores tipográficos que modifican sustancialmente el resultado del programa.
En general sin embargo hay más defectos en los caminos más inusuales de un grafo.
También resulta frecuente olvidar una condición o un término de una condición en un programa. Esto implica que al grafo del programa resultante le faltarán caminos.
Por ejemplo, supongamos que cometimos un
error en un código y escribimos:
if (x == y)
en vez de
if ((x == Y) & (x < 0))
El método CCC nos garantiza que generaremos dos tipos de prueba, una que entra a la condición cumpliendo x==y y otra cumpliendo x<y. La probabilidad de que entre el primer tipo de pruebas esté incluida una prueba que entre a la condición con x y y en cero (única posibilidad en que se detectaría que el camino tomado en la ejecución es incorrecto) es muy baja.
2.1.2 Un dato de entrada es poco representativo del conjunto de datos que ejercitan el mismo camino
El ejemplo anterior ilustra esta observación. El hecho de que de todos los pares de enteros <x,y> donde x=y, sólo el par <0,0> causa un comportamiento errado no requiere de más comentario.
En general podemos distinguir entre defectos visibles y defectos ocultos de un camino en un grafo de programa.
Un defecto visible asociado a un camino es aquel que produce un resultado erróneo para cualquier dato de entrada que ejercite ese camino.
Un defecto oculto asociado a un camino es aquel que particiona los datos de entradas que ejercitan ese camino en dos conjuntos no nulos: para cualquiera de los datos en una de las particiones produce un resultado erróneo y para los datos en la otra partición produce un resultado correcto.
Adicionalmente una fuente importante de defectos de programación proviene del síndrome "equivocarse por uno" o "equivocarse en la frontera". Es decir el programador:
La mayoría de las veces estos defectos son ocultos.
B. Beizer resumió estas observaciones en
su aforismo:
Bugs tend to lurk in comers and gather at boundaries.
Aforismo que podríamos traducir como:
Los defectos se esconden en los rincones olvidados y pululan
alrededor de los bordes.
2.1.3 Efectividad del método CCC
Si bien cada caso de prueba generado por el método CCC no tiene la misma probabilidad de encontrar un defecto, el conjunto de casos generados por el método CCC encuentran los defectos visibles. En particular obliga a generar casos de pruebas para caminos inusuales, donde tienden a haber muchos defectos.
El método CCC no es muy efectivo cuando se trata de encontrar defectos ocultos. Lamentablemente estos defectos son muy comunes e incluyen la importante clase de defectos de "frontera" y los defectos causados por caminos ausentes del grafo del programa. Realmente es cuestión de suerte que el método CCC detecte estas dos importantes clases de errores.
2.1.4 El método CCC con fronteras
Es fácil mejorar el método CCC para que tome en cuenta las fronteras. Sencillamente en vez de seleccionar cualquier entrada que ejercite un camino, seleccionamos la entrada que ejercite las fronteras del camino. Así si el camino requiere que los datos enteros de entrada x y y cumplan que:
1. Inicialmente x<y,
2. Más adelante, después de ejecutar parte del camino libre de condiciones, x=y+a
En este caso, el método de generación de
pruebas por cobertura ciclomática de fronteras de caminos (CCFC)
requiere que el caso de prueba para el camino tenga como datos de
entrada los enteros x1,y1 tales
que x1+1=y1 y que al ejecutar la
siguiente condición el nuevo valor x1'
sea igual a y'+a+1.
2.2 Eficiencia
La eficiencia de un método de generación de pruebas requiere que:
1. El número de
casos generados no explote combinatoriamente y,
2. El esfuerzo en
generar cada caso sea manejable.
El método CCC crece linealmente con el número de condiciones atómicas que incluya el programa por lo que cumple bien el primer requerimiento de eficiencia.
Adicionalmente en el método CCC los pasos de:
1. Generación del
grafo del programa,
2. Determinación del
número ciclomático y,
3. Generación de una
secuencia linealmente independiente de caminos
requieren relativamente poco esfuerzo y tiempo. De hecho, los tres pasos son fácilmente automatizables.
Sin embargo en ocasiones la situación se complica y dependiendo del programa puede requerir un esfuerzo no trivial:
1. Demostrar que un camino es contradictorio (no es posible generar un caso de prueba para ese camino);
2. Demostrar cuál es la máxima longitud de todas las secuencias linealmente independientes de un grafo, cuando esa longitud es menor que el número ciclomático;
3. Encontrar una secuencia linealmente independiente máxima (si las secuencias linealmente independientes tienen diferentes longitudes).
4. Encontrar los datos de entrada que ejercitan cada uno de los caminos de la secuencia linealmente independiente.
5. Determinar los resultados esperados de un caso de prueba sin ejecutar el software.
En dos experiencias vividas, una que
involucraba el desarrollo de un paquete de programación lineal
en paralelo y otra que desarrollaba un software de optimización
de servicios telemáticos en redes de telecomunicaciones, la
complejidad del cuarto factor impidió aplicar el método CCC.
3 Métodos caja negra
Los métodos caja negra más utilizados en el proceso de prueba de métodos son:
3.1 Partición de la entrada en clases de equivalencia
Supongamos que un método P cumple con la especificación:
{A(x) | B(x)} P {post(x, f(x))}
El dato de entrada x al método cumple una precondición que tiene la forma de una disyunción. Este método postula que debemos generar tres casos de prueba, con datos de entrada xl, x2, x3 que cumplan:
Ejemplo:
Dato de entrada: int x (un entero x cualquiera).
A(x): x positivo.
B(x): x número par.
Caso 1: x=3, porque 3 es un número positivo pero impar (otros casos podrían ser: 27, 53).
Caso 2: x= -6, porque -6 es un número par negativo ( otros casos podrían ser:-54, -106).
Caso 3: x= 18, porque 18 es un número par positivo (otros casos podrían ser: 2, 326).
Es decir, particionamos los datos en entrada en tres conjuntos y hacemos una prueba para un representante de cada conjunto. En general particionaremos la entrada en conjuntos representativos y se generará un caso de prueba por conjunto. También es posible realizar la partición según la postcondición. Por ejemplo si la especificación tiene la forma:
{pre (x)} P {A (x, f (x)) | B (x, f (x))}
se generan igualmente tres casos con entradas x1, x2, x3 pero donde:
Lo más usual es que la especificación tome la forma:
{pre(x)} P {(A(x) ® P1(x, f(x))) & (B(x) ® P2(x, f(x)))}
En cuyo caso las particiones serían nuevamente:
Finalmente pueden haber especificaciones del tipo:
{A (x) | B (x)}
f(x)
{[C(x) ® P1(x, f(x))] & [D(x) ® P2(x, f(x))]}
Analicemos las 24 (dieciséis) posibles combinaciones de los predicados A(x), B(x), C(x) y D(x) (utilizaremos "Ø" como símbolo de negación):
1. A(xl) & B(xl) & C(xl) & D(xl). Es candidato para caso de prueba.
2. Ø A(x2) & B(x2) & C(x2) & D(x2). Es candidato para caso de prueba.
3. A(x3) & Ø B(x3) & C(x3) & D(x3). Es candidato para caso de prueba.
4. A(x4) & B(x4) & Ø C(x4) & D(x4). Es candidato para caso de prueba.
5. A(x5) & B(x5) & C(x5) & Ø D(x5). Es candidato para caso de prueba.
6. Ø A(x6) & Ø B(x6) & C(x6) & D(x6). No genera caso de prueba (no se cumple precondición).
7. Ø A(x7) & B(x7) & Ø C(x7) & D(x7). Es candidato para caso de prueba.
8. Ø A(x8) & B(x8) & C(x8) & Ø D(x8). Es candidato para caso de prueba.
9. A(x9) & Ø B(x9) & Ø C(x9) & D(x9). Es candidato para caso de prueba.
10. A(x10) & Ø B(x10) & C(x10) & Ø D(x10). Es candidato para caso de prueba.
11. A(x11) & B(x11) & Ø C(x11) & Ø D(x11). Este es un caso muy interesante. Obsérvese que corresponde a una subespecificación del problema, en cuanto a que P1 y P2 pueden tomar ¡cualquier valor!.
12. Ø A(x12) & Ø B(x12) & Ø C(x12) & D(x12). No genera caso de prueba (no se cumple la precondición).
13. Ø A(x13) & Ø B(x13) & C(x13) & Ø D(x13). No genera caso de prueba (no se cumple la precondición).
14. Ø A(x14) & B(x14) & Ø C(x14) & Ø D(x14). Es candidato para caso de prueba.
15. A(x15) & Ø B(x15) & Ø C(x15) & Ø D(x15). Caso interesante (subespecificado, análogo a 11).
16. Ø A(x16) & Ø B(x16) & Ø C(x16) & Ø D(x16). No genera caso de prueba (no se cumple la precondición).
Puede ocurrir que existan propiedades adicionales como:
En tales situaciones, como es lógico, se
reduce el número de casos de prueba.
3.2 Análisis de fronteras
Es una variante del método de partición de la entrada, donde se escoge como representante de una partición al dato que esté en la "frontera" de la partición. Es decir, se aplica al Método de Partición de la Entrada una consideración similar a la que hicimos al llevar el método CCC al método CCFC.
Nota: Para el ejemplo que se muestra en la primera
especificación, sería interesante evaluar x = 0 como entrada,
ya que este número presenta ciertos paradigmas en cuanto a si es
positivo, si es negativo, o imparcial entre estas dos clases de
equivalencia (vale destacar que generalmente se toma a cero como
par ya que 0= 2*0, tiene la forma de un número par). Otros casos
relevantes serían: x = -2 (como frontera ya que 1 no
cumple con las precondiciones); x=1 (es el menor positivo pero
impar);
x = 2 (es el menor positivo par); tomar en consideración valores
de x que sean frontera dentro de la magnitud que pueden alcanzar
en un programa, es decir, el mayor entero par positivo (o
negativo) representativo, el mayor impar positivo (ya que el caso
negativo no cumple con las precondiciones).
3.3 Malicia
Se trata de una heurística y no de un método de generación de casos de pruebas. La heurística consiste en generar casos de pruebas que la experiencia en desarrollos sugiere que tienden a descubrir defectos.
Se recomienda usar la heurística para agregar casos de pruebas a los generados por algún método.
Por ejemplo, un valor de cero para un dato de entrada es siempre un buen candidato para formar parte de un caso de prueba, aunque no esté en la frontera de una partición de entrada.
En el caso de generar casos de prueba para una función que agrega un elemento a una lista, por malicia generaría los siguientes casos:
4 Un método híbrido de generación de pruebas
¿Cómo pueden combinarse los métodos caja negra y el método CCFC para mejorar la efectividad y la eficiencia de CCFC en la prueba de métodos de clases?
El método híbrido sugerido consta de los siguientes pasos:
1. Aplicar el método de caja negra para el análisis de frontera para crear un conjunto inicial de casos de prueba A0.
2. Aplicar la heurística de malicia para agregar nuevos casos, obteniendo el conjunto de casos A1.
3. Determinar si hay una secuencia de caminos en A1 que logra la cobertura ciclomática de fronteras de caminos del grafo del programa subyacente. Si no lo hace, generar los casos que faltan por el método CCFC. El conjunto resultante de casos de prueba lo denominaremos A2.
Las propiedades del método híbrido son:
1. Genera al menos los mismos casos que el método CCFC, por lo que su efectividad nunca será menor que la del método CCFC.
2. El uso del método de caja negra de análisis de frontera tiene dos ventajas. Para verlas es necesario observar que en muchos casos el método de caja negra en cuestión puede generar un subconjunto de casos que cumplen con la cobertura cíclomática de fronteras de caminos del grafo del programa. Ello se debe a que es probable que la estructura de condicionales del programa refleje la estructura del predicado de la especificación. Supongamos el caso simple de la siguiente especificación:
{A(X) | B(X)} P {post(x, f(x))}
Es muy probable que el programa incluya al menos las siguientes condicionales sobre los datos de entrada:
if (A(x)) ...
if (B(x))...
Si lo hace, los casos generados por el método de caja negra de análisis de fronteras tendrán una alta probabilidad de servir como casos CCFC. Las dos ventajas del método híbrido son:
(a) Suele ser más fácil encontrar datos de entrada a partir de una especificación de función que del análisis del código que lo implementa. Por ende, si la forma del predicado de la especificación induce una estructura en el programa, el método híbrido será más eficiente que CCFC.
(b) Si la especificación no induce una estructura en el programa, es probable que el método híbrido sea más efectivo que el método CCFC.
3. El método puede ser más efectivo que CCFC, por los casos adicionales generados por la heurística de malicia. Se sospecha que los casos generados por malicia no agregan un esfuerzo significativo al proceso de prueba en relación a su efectividad. También puede ocurrir que un caso generado por malicia sirva como caso en el método CCFC; de nuevo se sospecha que debe ser más fácil producir este caso por malicia que por análisis de código.
4. La identificación de los caminos faltantes puede ser automática. Existen herramientas de software que ejecutan un conjunto de casos de prueba y después reportan los caminos ciclomáticos que no han sido cubiertos. Tales herramientas usadas en conjunción con el método híbrido podrían incrementar notablemente la eficiencia del método híbrido en relación al método CCFC.
4.1 ¿A todos los métodos de todas las clases hay que aplicarle el método híbrido de generación de pruebas?
Algunos autores han observado que en los desarrollos orientados a objetos, las clases suelen tener métodos que son mucho más cortos y mucho menos complejos que las funciones incluidas en desarrollos diseñados y programados pensando en el paradigma de programación imperativa más tradicional. Al parecer esta tendencia es más pronunciada aún en desarrollos hechos en Smalltalk para los que se han reportado algunos casos de software donde la gran mayoría de los métodos ocupan menos de diez líneas de código.
Como heurística se recomienda no generar casos de prueba para métodos cuya complejidad ciclomática sea menor a cinco. En estos casos es más conveniente pasar directamente a realizar pruebas unitarias sobre la clase a que pertenece el método.
5 Las pruebas unitarias de clases
La unidad natural de prueba en un sistema orientado por objetos es la clase. El comportamiento de la clase lo especifica el diagrama de estados del modelo dinámico. Por ello luce razonable utilizar el diagrama de estados como punto de partida para las pruebas unitarias de una clase.
El método de generación de pruebas unitarias que presentaremos es el método de cobertura ciclomática de fronteras de caminos del autómata presentado por el diagrama de estados.
El autómata puede considerarse como un multigrafo orientado etiquetado con bucles. Afortunadamente el concepto de complejidad ciclomática se extiende fácil y naturalmente de los grafos a los multigrafos. De hecho es fácil demostrar que el número de regiones de un multigrafo planar sigue siendo:
E-N+2
donde E es el número de aristas del multigrafo y N es el número de vértices. Por ende el método CCC se extiende naturalmente a los autómatas de un diagrama de estados. Sólo debemos observar que un caso de prueba consistirá en obligar a un objeto de la clase estudiada a realizar la secuencia de transiciones asociadas con un camino identificado por el método CCC extendido a multigrafos.
Para ello requerimos:
1. Expandir recursivamente todo autómata anidado.
2. Explicitar las transiciones implícitas, si existen (típicamente los eventos que pueden ocurrir en un estado del objeto pero que no disparan una transición).
3. Eliminar todas las transiciones épsilon, identificando los estados iniciales y los estados finales resultantes.
4. Reformular el estado inicial, creando un nuevo nodo que precederá a los estados iniciales. Agregar transiciones de ese nuevo estado inicial a los estados iniciales previos, etiquetando cada transición con un constructor de la clase. Esto permitirá generar casos de prueba para los diferentes constructores de la clase.
5. Reformular un nuevo estado final, creando un nuevo nodo y agregando transiciones etiquetadas de los estados finales previos al nuevo estado final. Cada etiqueta consta de uno de los métodos destructores de la clase.
6. Observar que un camino es una secuencia de transiciones que comienza en el nuevo estado inicial y termina en el nuevo estado final.
7. Si una condición asociada a una transición es una disyunción de predicados como por ejemplo [A(x) | B(x)], replicar la transición, una vez por término del predicado.
Las transiciones resultantes pueden estar asociadas a eventos, condiciones o ambas.
Si el evento asociado es parametrizado, evidentemente construir el caso de prueba implica proporcionar un valor a ese parámetro.
Finalmente debemos incorporar un análisis de fronteras. Las fronteras vienen determinadas por las condiciones y las acciones condicionales asociadas a las transiciones. Supongamos que una condición asociada a una transición es:
[(x >1) & (x < 9)]
En tal tipo de escenario deberíamos crear cuatro casos de prueba, donde en el estado origen de la transición x tome los valores: 1, 2, 8 y 9 respectivamente. Es importante recordar que la intención del análisis de frontera es determinar si no se cometió un error en la ubicación de la frontera.
Una acción condicional tiene la forma:
e/si C(x) entonces A1 sino A2
Evidentemente aquí hay dos transiciones disfrazadas como una sola y hay que colocar ambas:
e[C(x)]/ A1
e[not C(x)]/ A2
Finalmente complemente el método CCFC de pruebas unitarias que esbozamos con pruebas generadas con malicia. Esta etapa es importante ya que no tenemos otra forma de mejorar las (escasas) probabilidades de hallar los caminos que faltan.
Object-Oriented Testing (Roger S. Pressman Web Site)
How to do Good Testing (Brian Marick)
Notas sobre Object-Oriented Testing: Parte 1, Parte 2 (Brian Marick)
OO Testing: Mito y Realidad (Robert V. Binder)
Classic Testing Mistakes (Brian Marick)
Testing OO Software Using the Category-Partition Method (Jeff Offutt and Alisa Irvine )
Testing Object-Oriented Software (Stéphane Barbey)
Roger S. Pressman, Addison-Wesley 1992.
[ Anterior ] | [ Indice ] | [ Siguiente ]