Universidad Simón Bolívar
Dpto. de Computación y T.I.
Sistemas de Programas
 

Pruebas de sistemas orientados a objetos


Introducción

En general, el software es un artefacto complejo, voluminoso y relativamente frágil, en el sentido que puede bastar un defecto para que falle de manera catastrófica.

La elaboración de un software es un proceso propenso a errores: los desarrolladores pueden cometer errores en la elicitación de requerimientos, en la elaboración de los artefactos del análisis y el diseño, en la codificación y hasta en el proceso mismo de prueba.

Por ello, es importante revisar la consistencia intra- e inter-artefactos. El sueño dorado de cualquier empresa que depende de software es una herramienta que tome por entrada los requerimientos y produzca como salida el software apropiado, haciendo así obsoleto al desarrollador de software; el el desarrollador de software sueña en una herramienta que automatice los pasos más mecánicos para pasar de un artefacto a otro y que verifique automáticamente la consistencia entre esos artefactos.

Cuando se valida o verifica la consistencia de un artefacto no ejecutable respecto a otro artefacto no ejecutable, se habla de un proceso de inspección; cuando se valida un artefacto ejecutable (típicamente el código del software) se habla de un proceso de prueba (testing).

Hay muchos tipos de pruebas. Entre ellas podemos mencionar:

Por razones de tiempo, en este curso nos limitaremos a probar el software contra su Modelo de Uso. Este tipo de  pruebas pueden considerarse  como  un tipo de prueba de  requerimientos funcionales del sistema.

Cuán exhaustivamente queremos realizar las pruebas depende del nivel de calidad a que aspiremos, el tiempo disponible para las pruebas y el riesgo que estemos dispuestos a aceptar al ejecutar el software. Evidentemente un software que desarrollo para divertirme no necesita someterse al mismo proceso de pruebas que un software médico que se usará en 150 países y cuyas fallas pueden ser, literalmente, fatales. En este curso nos limitaremos a realizar pruebas sencillas, poco exigentes, apropiadas al caracter de prototipo del desarrollo; en la asignatura Ingeniería de Software 3 (CI-4713) se presenta un proceso de prueba más exigente.

En todo proceso de prueba podemos distinguir al menos cuatro etapas:

Planificación del proceso de prueba

La planificación del proceso de prueba incluye:
  1. El nombre del coordinador del proceso de prueba;
  2. El alcance del proceso de pruebas;
  3. Las características del ambiente de prueba;
  4. Los nombres de los responsables de las actividades de prueba;
  5. El calendario y esfuerzo previsto (schedule) para las actividades.
En el alcance del proceso de pruebas se indica la configuración del software que se somete a pruebas, los tipos  de pruebas que se llevarán a cabo, el grado de exigencia para cada tipo de prueba y el criterio de terminación para las pruebas. El criterio de terminación explicita las condiciones que debe satisfacer un proceso de pruebas para considerarse satisfactorio. Algunos criterios posibles son: Es importante saber, por ejemplo que un software debe probarse bajo Windows 98  pero no bajo Windows ME. Por ello, la sección correspondiente a las características del ambiente de prueba permite  registrar las consideraciones especiales que se deben hacer para ejecutar correctamente los casos de prueba (por ejemplo, revisar que se encuentre la versión 1.5  del archivo a.txt en el directorio ~teruel/pruebas), así como dejar constancia de las versiones  de los software de apoyo (Sistema de Operación, compiladores, generadores GUI etc.) y los modelos de máquinas que deben usarse en las pruebas. Esto permite reducir las tareas de depuración debidos a defectos de configuración.
 

Elaboración de casos de prueba

Un caso de prueba elaborado incluye:
  1. Un nombre que identifica el caso;
  2. Sus entradas: es la secuencia exacta de acciones que debe realizar el usuario para ejecutar ejecutar el caso de prueba. Note que si las acciones involucran introducir valores, deben especificarse los  valores exactos que deben introducirse;
  3. Su salida esperada: Los resultados exactos observables que debe producir el sistema al ejecutar el caso de prueba.
Idealmente debemos elaborar, en el menor tiempo posible,  un conjunto mínimo de casos de prueba que garantice el nivel de calidad deseado para el software. En la realidad, debemos conformarnos con elaborar, en un tiempo  razonable, un conjunto reducido de casos de prueba, tal que si el proceso de prueba cumple con el criterio de terminación, la probabilidad de satisfacer el nivel de calidad deseado es suficientemente alto.

Frecuentemente los casos de prueba se generan:

La mayor parte de los defectos que se introducen en el software no son defectos aleatorios. Podemos distinguir algunos modelos de defectos  que buscan caracterizar el tipo y número de defectos que se inyectan en el desarrollo del software.

Por ejemplo, un modelo de defectos muy aceptado es el que postula que una proporción importante de defectos son defectos en el manejo de fronteras. Típicamente la frontera se fija incorrectamente, por lo que valores que deben caer dentro de una frontera, caen fuera de ella o vice versa. Así, para poner un caso sencillo, si un valor de entrada debe ser un valor entero entre 1 y 10, el software valida incorrectamente el valor introducido, permitiendo valores entre 0 y 10, 1 y 11 o 0 y 11 (es muy frecuente dentro de los defectos de frontera, la equivocación por una unidad, defecto denominado en Inglés como off-by-one).

Por ende, si aceptamos el modelo de defectos en el manejo de fronteras, es conveniente incluir casos de prueba que, valga la redundancia, pongan a prueba las fronteras, es decir que sospechen que el desarrollador cometió un error de fronteras en algún lado de su software.

El tester analiza pues los artefactos del desarrollador, buscando pistas que le sugieren la aplicabilidad de modelos de defectos. Así si el tester encuentra en un contrato de evento de sistema, una precondición a validar que indica que a>3, utilizará tal expresión como pista para sospechar que el software valida incorrectamente la frontera por lo que elabora dos casos de prueba, uno donde introduce el valor a=3 y otro donde introduce a=4.  Es importante hacer notar que estos dos casos de uso están incompletos, dado que aún no he explicitado la salida esperada del caso (a veces denominaremos especificación de un caso de prueba, a un caso incompleto o en el que se caracteriza a una entrada sin precisarle un valor específico --p. ej. x>5). En particular las pistas para el modelo de defectos de fronteras pueden encontrarse en muchos artefactos del desarrollo:

En este curso prestaremos particular atención al modelo de defectos de fronteras.

Ejecución de los casos de prueba

Para cumplir apropiadamente con esta etapa, llevaremos una bitácora de pruebas donde se anotará:

Evaluación de los resultados de las pruebas

En esta etapa queremos aplicar el criterio de terminación del proceso de pruebas para saber si podemos darlo por concluido exitosamente. En caso negativo, debemos planificar el proceso de depuración y nuevas pruebas (quién se encarga de qué, para cuándo). Al terminar la depuración podemos: Para complicar las cosas, hay que tomar en cuenta que, a veces, las discrepancias entre la salida esperada y la la obtenido se debe a ¡defectos en los casos de prueba!

Finalmente es conveniente escribir un reporte final para la gerencia, de modo que queden estadísticas sobre el proceso de prueba para utilizarse en análisis post-hoc (a veces denominados post-mortem) del proyecto de desarrollo. Típicamente la gerencia quiere saber en forma precisa y resumida:

Casos de uso como punto de partida para elaborar casos de prueba

Para concretar algunas de las ideas mencionadas previamente, en este curso elaboraremos casos de prueba  que permitan validar si el software desarrollado cumple con las funciones descritas en sus casos de uso. El nivel de exigencia que manejaremos para el proceso de proceso es relativamente débil, cónsono con el desarrollo de prototipos o pilotos de sistemas poco críticos.

Para elaborar los casos de prueba, y dependiendo del grado de exigencia del proceso de prueba, haremos uso de los


Utilizaremos, como es costumbre en este curso, el ejemplo del sistema de punto de venta del texto de Larman para ilustrar los conceptos.

Los casos de uso pueden escribirse de modo que cada caso de uso documenta o especifica un uso posible del sistema o pueden corresponder a sesiones de uso del sistema.

Comenzaremos suponiendo que tenemos acceso a cada uno de los casos de uso que describen un uso del sistema. Queremos por ende, revisar si el sistema se comporta según lo especificado en ese caso de uso.
 

Escenarios de uso

Cada caso de uso describe varios escenarios  de uso, entre los que se distinguen: Los escenarios correspondiente al curso normal del caso de uso son aquellos que, como su nombre lo indican, siguen al pie de la letra el curso normal del caso de uso. Para probar uno de estos escenarios debemos introducir eventos al sistema parametrizados de tal forma de no salirse del curso normal ni de producir excepciones que interrumpan ese curso. Note que los cursos normales pueden incluir elementos de control de flujo como acciones condicionadas o iteradas, como por ejemplo hacer un pago en efectivo, cheque o con tarjeta de crédito (acciones condicionales) o comprar varios tipos diferentes de productos (acciones iteradas). Para poder probar un curso normal tenemos entonces que indicar no sólo los eventos del sistema a introducir y el valor de sus parámetros, sino también si se lleva a cabo o no cada acción condicionada y el número de veces que se llevará a cabo las acciones iteradas.

Con la notación de casos de uso utilizada en el curso, puede resultar dificil identificar los escenarios que producen al menos una excepción.  Para ello, puede ser necario revisar los contratos asociados a los eventos del sistema mencionados en los casos de uso.

Para elaborar los casos de prueba debemos decidir cuáles de los escenarios de uso queremos ejercitar.
 

Grados de exigencia

Debemos decidir el grado de exigencia con que queremos probar un caso de prueba. En este curso consideraremos dos niveles: Diremos que un software ha sido débilmente sometido a pruebas contra un caso de uso ssi se ejecutan casos de prueba que:

Ejemplo de un grado de exigencia débil

Elaboraremos un conjunto de especificaciones de  casos de prueba para probar débilmente al caso de uso Comprar Productos para el primer incremento del sistema de punto de venta.

Recuerde que en el primer incremento, no se actualiza el inventario de productos, no se puede cancelar una venta en progreso,  sólo se puede pagar en efectivo, y se supone que el Cajero siempre tiene vuelto para el cliente.

Note que el escenario normal de este caso de uso contiene:

El caso de uso contiene una opción (introducir un producto cuyo código no está registrado en el catálogo). Note que llevar a cabo la opción produce la única excepción prevista para el caso de uso y en los contratos. Por ende,  nos basta con los siguientes casos de prueba para lograr una prueba débil del caso de uso:
  1. Un caso de prueba que ejercite el escenario normal y  produzca cero iteraciones de la acción iterable;

  2.  
  3. Un caso de prueba que ejercite el escenario normal, produzca exactamente una iteración de la acción iterable y no se ejecute la acción condicionada;

  4.  
  5. Un caso de uso que ejercite el escenario normal, produzca exactamente una iteración de la acción iterable y no se ejecute la acción condicionada;

  6.  
  7. Un caso de prueba que ejercite el escenario normal y  produzca dos o más iteraciones de la acción iterable;

  8.  
  9. Un caso de prueba que ejercite la opción al curso normal.
Precisemos  cada especificación de caso de prueba:
  1. Un caso de prueba en el que no se compren productos (es decir se acciona terminarVenta() como primer evento del sistema. Una interesante discusión se suscita por dos aspectos:
    1. ¿Puede darse este evento sin que el caso de prueba incluya un evento introducirProducto() previo?;
    2. ¿Puede darse este evento sin que se incluya un evento pagar() posterior?
    Estas preguntas ilustran algunos de los problemas que pueden surgir por utilizar lenguaje informal para describir los casos de uso.
     
  2. Un caso de prueba en el que se compre exactamente un producto. Las acciones que deben introducirse en este caso de prueba son:
    1. Un evento introducirProductos(cup, cant) donde cup corresponde a un código catalogado y cant es exactamente  uno (el cajero no debe tener que introducir la cantidad de productos);
    2. Un evento terminarVenta();
    3. Un evento pagar(monto) donde el monto sea mayor o igual al total a pagar por la compra del producto introducido;
  3. Un caso de prueba en el que se compre más de un producto del mismo tipo. Las acciones que deben introducirse en este caso de prueba son:
    1. Un evento introducirProductos(cup, cant) donde cup corresponde a un código catalogado y cant es mayor que uno;
    2. Un evento terminarVenta();
    3. Un evento pagar(monto) donde el monto sea mayor o igual al total a pagar por la compra de los productos introducidos;
  4. Un caso de prueba en el que  se compren varios productos. Este escenario lo satisface las siguientes acciones:
    1. Un evento introducirProductos(cup, cant) donde cup corresponde a un código catalogado y cant es mayor o igual que uno;
    2. Un evento introducirProductos(cup', cant') donde cup'  corresponda a un código catalogado diferente a cup y cant' es mayor o igual a uno;
    3. Un evento terminarVenta();
    4. Un evento pagar(monto) donde el monto sea mayor o igual al total a pagar por la compra de los productos introducidos;
  5. Un caso de prueba en el que se ejecuta el curso alterno correspondiente a introducir un código no catalogado:
    1. Un evento  introducirProductos(cup, cant) donde cup corresponde a un código no catalogado y cant es mayor o igual que uno;
    Se presenta una discusión interesante entorno a si el caso de uso debe incluir algún otro evento:
    1. terminarVenta(): podría argumentarse que no debe incluirse, si el primer incremento se limita a lanzar una excepción del cual no se recupera (¡poco elegante!)
    2. pagar(): la inclusión de este evento depende de la discusión anterior, si el caso no incluye un terminarVenta(), es más dificil argumentar que debe incluir un pagar(), pero si incluye un terminarVenta(), puede argumentarse a favor o en contra de obligar a incluir un evento pagar().

Grado medio de exigencia

Diremos que un caso de uso ha sido medianamente sometido a prueba si: La idea de la frontera de un parámetro proviene por analogía de variables restringido a un intervalo como por ejemplo, una variable entera x cuyos valores están restringidos al intervalo [1..10]. Para poner a prueba la frontera es conveniente hacer pruebas en los que: Note que las variables tipo Número o int siempre tienen al menos dos fronteras (el mínimo valor representable y el máximo valor representable).

Las variables pueden tener más de dos fronteras. Por ejemplo

    x pertenece_a [1..10, 20..30]
puede considerarse como una variable con cuatro fronteras (los valores extremos válidos serían 1,10,20,30).

En general si las restricciones sobre una variable puede expresarse como una conjunción de predicados:

P1(x) && P2(x) && P3(x)
consideraremos que cada predicado representa al menos una frontera. Por ende, tendremos que tratar de generar pruebas donde se prueben valores de frontera válidos, es decir que cumplan: y valores vecinos a la frontera, es decir que no cumplan con los predicados. Esto lo haremos tratando de falsificar un predicado a la vez (para evitar explosiones combinatorias), por lo que necesitaremos intentar generar casos donde:


Si tenemos dos parámetros x,y independientes (es decir que el valor de uno de ellos no depende del valor que se le asigne al otro), entonces, para evitar la explosión combinatoria de valores, no exigeremos  combinar exhaustivamente los n1 valores de prueba de frontera de x con los n2 valores posibles de frontera de y para generar n1*n2 casos.

Los valores de los parámetros se combinan de la siguiente forma:

En general, si tenemos n variables independientes, cada una de las cuales tiene ci fronteras, necesitamos maxi_in[1..n]{ci} casos que combinen valores válidos de frontera y sumatoriai in [1..n]{ci} casos para valores vecinos a fronteras.

La dependencia entre variables puede reducir el número de casos de prueba y afectar las combinaciones permitidas de valores en los casos de uso.

Por ejemplo, podemos recurrir a los siguientes casos para probar medianamente al caso de uso Comprar Productos para el primer incremento del sistema de punto de venta.

  1. Casos de prueba en el que no se compren productos. Hay dos posibilidades que conviene probar con casos de prueba diferentes:
    1. Se acciona terminarVenta() como primer evento del sistema. Como no recurre a eventos con parámetros, podemos usar el mismo caso para probar el sistema débilmente o medianamente.
    2. Se acciona el evento introducirProducto(cup, 0), donde cup corresponde a un código catalogado, seguido por la acción terminarVenta().
  2. Casos de prueba en los que se compra exactamente un producto. Todos estos casos comienzan por realizar las siguientes acciones:
    1. Un evento introducirProductos(cup, cant) donde cup corresponde a un código catalogado y cant es exactamente uno (el cajero no debe tener que introducir la cantidad de productos);
    2. Un evento terminarVenta();
    y se distinguen por el valor que le proporcionan al parámetro monto del evento pagar:
    1. Un evento pagar(monto) donde el monto sea el mayor valor posible menor al total a pagar por la compra del producto introducido (la interfaz del usuario puede impedir que se pueda introducir tal valor, pero hay que revisar que efectivamente lo haga);
    2. Un evento pagar(monto) donde el monto sea igual al total a pagar por la compra del producto introducido;
    3. Un evento pagar(monto), donde el monto sea igual al máximo valor que se pueda introducir por la interfaz;
  3. Casos de prueba en el que se compre más de un producto del mismo tipo. Por ejemplo:
    1. Caso 1
      1. Un evento introducirProductos(cup, cant) donde cup corresponde a un código catalogado y cant es exactamente igual a dos;
      2. Un evento terminarVenta();
      3. Un evento pagar(monto) donde el monto sea igual al total a pagar por la compra de los productos introducidos;
    2. Caso 2
      1. Un evento introducirProductos(cup, cant) donde cup corresponde a un código catalogado y cant es igual al mayor valor que se pueda introducir;
      2. Un evento terminarVenta();
      3. Un evento pagar(monto) donde el monto sea igual al total a pagar por la compra de los productos introducidos.
      Note que en este caso monto y cant no son variables independientes (el monto debe ser mayor o igual al producto cant*precioUnitario). Por ende introducir el mayor valor posible en cant, puede obligar este caso a producir una excepción, si ese producto no es representable. Por ende puede argumentarse que cualquiera que sea el valor asignado a monto, este caso no constituye una prueba real para ese valor (pues no se llega a aplicar).
  4. Un caso de prueba en el que  se compren varios productos. Este escenario lo satisface las siguientes acciones:
    1. Un evento introducirProductos(cup, cant) donde cup corresponde a un código catalogado y cant es mayor o igual que uno;
    2. Un evento introducirProductos(cup', cant') donde cup'  corresponda a un código catalogado diferente a cup y cant' es mayor o igual a uno;
    3. Un evento terminarVenta();
    4. Un evento pagar(monto) donde el monto sea mayor o igual al total a pagar por la compra de los productos introducidos;
    Note que cant, cant' están en el rango [1..maxIntroducible] y monto está en el rango [totalCalculado..maxIntroducible]. Podemos cumplir con una prueba mediana con
    1. cant, cant' valen 1, monto es igual al totalCalculado.
  5. Un caso de prueba en el que se ejecuta el curso alterno correspondiente a introducir un código no catalogado:
    1. Un evento  introducirProductos(cup, cant) donde cup corresponde a un código no catalogado y cant es mayor o igual que uno;

Prueba de sesiones de uso

Probar sesiones de uso significa probar la robustez de las sesiones, es decir, queremos diseñar casos de prueba para determinar que los usos del sistema permitidos dentro de una sesión no interfieren entre si y respetan restricciones de orden o precondiciones entre ellos. Típicamente queremos revisar aspectos como:

Ejemplo

En el desarrollo  del sistema de punto de venta, los usos, para un actor tipo Cajero, son:
  1. Comprar productos;
  2. Devolver productos;
  3. Abrir terminal;
  4. Cerrar terminal.
Estos usos se agrupan en una sesión de uso de terminal de punto de venta. Al probar la sesión queremos probar características como: Por otro lado, tambien tenemos sesiones de configuración del sistema, en las que interviene el actor Administrador y, eventualmente, sesiones de análisis de venta para el actor Analista de Ventas.
 

Observaciones y recomendaciones finales


Esta página fue creada el 19 de junio de 2001 por el Prof. Alejandro Teruel.
Ultima actualización: 22 de junio 2001.
Por favor dirija sus comentarios al Prof. Alejandro Teruel.