Un paso importante en la prueba del software es desarrollar datos de prueba útiles y exhaustivos. Una manera probada de conseguir esto es utilizar técnicas de particionamiento de datos (también llamadas análisis de dominio).
El particionamiento de datos es una manera efectiva de seleccionar valores de prueba útiles y conseguir la mejor cobertura posible. Este tema se centra en la utilización de técnicas de particionamiento de datos y en su aplicación en sistemas orientados a objetos, específicamente en sistemas que contienen parámetros complejos de tipo objeto y entradas externas o basadas en contexto.
En general, puede seguir los pasos siguientes para particionar los datos de prueba:
Los ejemplos que se utilizan en este tema se toman de la aplicación Money de JUnit y se centran concretamente en el método equals de la clase Money. El código para este método se muestra en el listado de código siguiente:
public class Money implements IMoney { private int fAmount private String fCurrency; public boolean equals(Object anObject) { if (isZero()) if (anObject instanceof IMoney) return ((IMoney)anObject).isZero(); if (anObject instanceof Money) { Money aMoney= (Money)anObject; return aMoney.currency().equals(currency()) && amount() == aMoney.amount(); } return false; }
Conceptos relacionados
Conjuntos de datos
Tablas de datos de prueba
El primer paso en la definición de datos de prueba es entender qué datos utiliza el componente que se prueba (CUT). Utilizando el método equals como ejemplo, puede ver que este método adopta un único argumento (anObject) de tipo Object:
public boolean equals(Object anObject) {
El método equals es un método de instancia porque no está definido como estático. Por tanto, además del parámetro anObject, el CUT también puede utilizar datos de la instancia de objeto en la que se invoca el objeto.
Por sí mismos, hay dos parámetros de prueba que pueden afectar a lo que sucede al invocar el método equals: el objeto en el que se invoca el método (this) y el argumento del método (anObject). Si llamamos a al primer parámetro y b al argumento del método (anObject), esto se representaría en el código como a.equals(b). Aquí puede observar que para probar el método equals se variarían los datos asociados con a y b.
Las dos posibles entradas (a y b) son tipos de datos de objeto. La primera entrada (a) siempre es del tipo Money y se puede definir en términos de los atributos fCurrency y fAmount. Estos atributos de definición son los parámetros de un constructor público para la clase Money. De este modo, para variar la primera entrada, ser variarían los atributos de definición del objeto (fCurrency y fAmount). Se proporcionaría un rango de enteros, probablemente con números negativos, positivos y con el cero. A continuación, se variarían los tipos de moneda mediante identificadores de moneda válidos y no válidos.
Debido a que la segunda entrada (b) es del tipo de datos Object, casi todas las instancias de clase se pueden pasar como valor válido. Puede efectuar una comparación significativa si se pasa un objeto Money, pero también puede intentar pasar datos de otros tipos de datos que no tenga sentido comparar y ver cómo responde el método.
Las particiones de datos deben definirse de tal manera que se puedan revelar el mayor número de defectos potenciales. En este ejemplo, hay dos parámetros de entrada y ambos son del tipo de datos de objeto. La mejor forma de definir particiones para los parámetros de un objeto es considerar sus estados abstractos. El estado abstracto de un objeto se suele definir como una restricción en los valores de sus atributos. Por ejemplo, algunos estados abstractos posibles para el primer parámetro de entrada (a) sería:
No obstante, en este caso, según sus conocimientos de lo que realiza el método equals, una partición debe ser suficiente para el primer parámetro (a). Esto se debe a que el valor de retorno para este método no está directamente relacionado con ningún valor concreto para este parámetro. Como consecuencia, puede elegir cualquier valor de prueba y limitarse a crear una partición de datos. Posteriormente, al definir las clases de equivalencia, puede crearse diversidad en la prueba mediante la utilización de valores diferentes para este parámetro.
En el caso del segundo parámetro de entrada (b), es difícil definir un estado abstracto porque el tipo de parámetro de Object es la superclase de todos los tipos y, por ello, se puede utilizar casi cualquier clase. En este caso, primero debe definir particiones de datos en términos de tipos de parámetros (esto también sería cierto para cualquier parámetro cuyo tipo sea una clase abstracta o una interfaz). Para este parámetro, podría identificar tipos como:
Así, algunas particiones de datos posibles para el parámetro de entrada b son Mismo tipo, Otros y Nulo.
A continuación, podría subdividir las particiones de datos para el segundo parámetro de entrada (b). Para tipos incompatibles u objetos nulos, probablemente no es necesario efectuar más subdivisiones: equals debe devolver false sistemáticamente.
Para Mismo tipo, hay varias particiones subdivididas posibles:
El paso siguiente sería definir las clases de equivalencia y los valores. Una clase de equivalencia es un conjunto de valores de entrada de los que se espera que todos invoquen el mismo comportamiento. Si algún valor único de la clase de equivalencia genera una prueba que falla, el resto de valores de la clase de equivalencia debe generar pruebas que fallan. Del mismo modo, si algún valor único de la clase de equivalencia genera una prueba válida, el resto de valores de la clase de equivalencia debe generar pruebas válidas.
Con los productos Rational Developer, defina clases de equivalencia en las tablas de datos de prueba, denominadas aquí conjuntos de datos. Mediante la utilización de particiones de datos que se acaban de definir, podría crear en primer lugar clases de equivalencia que manejan la comparación entre las cuatro particiones "Same Type" y la partición "Any Attribute Value". En la siguiente captura de pantalla se muestra una tabla de datos de prueba con dos de estas cuatro clases de equivalencia:
Cada clase de equivalencia vuelve a utilizar la misma partición de datos ("any attribute value") para el primer parámetro de entrada. Sin embargo, para crear diversidad en esta prueba, cada clase de equivalencia utiliza valores diferentes de esta misma partición de datos.
A continuación, podría crear clases de equivalencias para los datos nulos y particiones de tipo de datos no compatibles, como se muestra en la siguiente captura de pantalla:
Finalmente, puede utilizar conjuntos y rangos para proporcionar muchas más combinaciones de datos para la prueba, como se muestra en la siguiente captura de pantalla. Para la cantidad, podría proporcionar un rango de enteros, con números negativos, positivos y con el cero. Para la moneda, podría intentar utilizar un conjunto de identificadores de moneda válidos y no válidos. Al efectuar esto, aumenta en gran manera el número de combinaciones de datos en la prueba y, como resultado, puede aumentar la cobertura de la prueba y la probabilidad de descubrir defectos.
No obstante, tenga en cuenta que la utilización de conjuntos y rangos puede crear un gran número de pruebas individuales que pueden tardar mucho en ejecutarse.
En este ejemplo se han tratado modos de definir particiones de datos y clases de equivalencia para probar un método. Cuando se define un caso práctico que invoca muchos métodos, o bien cuando los métodos utilizan objetos, puede seguir los mismos procedimientos. Sin embargo, en estos casos, es necesario reducir el número de parámetros de entrada para que el problema sea más manejable.
En estos casos, es muy importante utilizar estados abstractos para gestionar objetos. Siempre debe definir estados abstractos antes de intentar crear particiones de los atributos de definición de un objeto. Incluso cuando se considera un objeto como único parámetro de entrada, se puede acabar fácilmente con docenas de parámetros de entrada.
Esto significa que al tratar con casos prácticos complejos, debe prestarse mucha atención al elegir los parámetros de entrada. En general, para gestionar esta complejidad, intente reducir el problema a menos de 10 variables.