pruebas
.Para asegurar en la medida de lo posible el correcto funcionamiento y la calidad del software se suelen utilizar distintos tipos de pruebas, como pueden ser las pruebas unitarias, las pruebas de integración, o las pruebas de regresión.
A lo largo de este capítulo nos centraremos en las pruebas unitarias, mediante las que se comprueba el correcto funcionamiento de las unidades lógicas en las que se divide el programa, sin tener en cuenta la interrelación con otras unidades.
La solución más extendida para las pruebas unitarias en el mundo Python es unittest, a menudo combinado con doctest para pruebas más sencillas. Ambos módulos están incluidos en la librería estándar de Python.
doctest
Como es de suponer por el nombre del módulo, doctest permite combinar las pruebas con la documentación. Esta idea de utilizar las pruebas unitarias para probar el código y también a modo de documentación permite realizar pruebas de forma muy sencilla, propicia el que las pruebas se mantengan actualizadas, y sirve a modo de ejemplo de uso del código y como ayuda para entender su propósito.
Cuando doctest encuentra una línea en la documentación que comienza con >>> se asume que lo que le sigue es código Python a ejecutar, y que la respuesta esperada se encuentra en la línea o líneas siguientes, sin >>>. El texto de la prueba termina cuando se encuentra una línea en blanco, o cuando se llega al final de la cadena de documentación.
Tomemos como ejemplo la siguiente función, que devuelve una lista con los cuadrados de todos los números que componen la lista pasada como parámetro.
Cita:
Def cuadrados(lista):
Calcula el cuadrado de los números de una lista
Return [n ** 2 for n in lista].
Podríamos crear una prueba como la siguiente, en la que comprobamos que el resultado al pasar la lista [0, 1, 2, 3] es el que esperábamos.
Cita:
Def cuadrados(lista):
Calcula el cuadrado de los números de una lista.
>>> l = [0, 1, 2, 3]
>>> cuadrados(l)
[0, 1, 4, 9]
Return [n ** 2 for n in lista].
Lo que hacemos en este ejemplo es indicar a doctest que cree un lista l con valor [0, 1, 2, 3], que llame a continuación a la función cuadrados con l como argumento, y que compruebe que el resultado devuelto sea igual a [0, 1, 4, 9].
Para ejecutar las pruebas se utiliza la función testmod del módulo, a la que se le puede pasar opcionalmente el nombre de un módulo a evaluar (parámetro name). En el caso de que no se indique ningún argumento, como en este caso, se evalúa el módulo actual.
Cita:
Def cuadrados(lista):
Calcula el cuadrado de los números de una lista.
>>> l = [0, 1, 2, 3]
>>> cuadrados(l)
[0, 1, 4, 9]
Return [n ** 2 for n in lista]
Def _test():
Import doctest.
Doctest, testmod()
If __name__ == __main__:
Test().
En el caso de que el código no pase alguna de las pruebas que hemos definido, doctest mostrara el resultado obtenido y el resultado esperado. En caso contrario, si todo es correcto, no se mostrara ningún mensaje, a menos que añadamos la opción -v al llamar al script o el parámetro verbose=true a la función tesmod, en cuyo caso se mostraran todas las pruebas ejecutadas, independientemente de si se ejecutaron con éxito.
Este sería el aspecto de la salida de doctest utilizando el parámetro -v.
Cita:
Trying:
L = [0, 1, 2, 3]
Expecting nothing.
Ok.
Trying:
Cuadrados(l)
Expecting:
[0, 1, 4, 9]
Ok.
2 Items had no tests:
_main__
_main__._test.
1 Items passed all tests:
2 tests in __main__.cuadrados.
2 tests in 3 Items.
2 passed and 0 failed, test passed.
Ahora vamos a introducir un error en el código de la función para ver el aspecto de un mensaje de error de doctest. Supongamos, por ejemplo, que hubiéramos escrito un operador de multiplicación (*) en lugar de uno de exponenciación (**).
Cita:
Def cuadrados(lista):
Calcula el cuadrado de los números de una lista.
>>> l = [0, 1, 2, 3]
>>> cuadrados(l)
[0, 1, 4, 9]
Return [n * 2 for n in lista]
Def _test():
Import doctest.
Doctest, testmod()
If __name__ == __main__:
Test().
Obtendríamos algo parecido a esto.
Cita:
************************************************** *******
File ejemplo, py, line 5, in __main__.cuadrados.
Failed example:
Cuadrados(l)
Expected:
[0, 1, 4, 9]
Got:
[0, 2, 4, 6]
************************************************** *******
1 Items had failures:
1 of 2 in __main__.cuadrados.
***test failed*** 1 failures.
Como vemos, el mensaje nos indica que ha fallado la prueba de la línea 5, al llamar a cuadrados(l), cuyo resultado debería ser [0, 1, 4, 9], y sin embargo, obtuvimos [0, 2, 4, 6].
Veamos por último cómo utilizar sentencias anidadas para hacer cosas un poco más complicadas con doctest. En el ejemplo siguiente nuestra función calcula el cuadrado de un único número pasado como parámetro, y diseñamos una prueba que compruebe que el resultado es el adecuando para varias llamadas con distintos valores. Las sentencias anidadas comienzan con . en lugar de >>>.
Cita:
Def cuadrado (num):
Calcula el cuadrado de un número.
>>> l = [0, 1, 2, 3]
>>> for n in l:
, cuadrado (n)
[0, 1, 4, 9]
Return num ** 2
Def _test():
Import doctest.
Doctest, testmod()
If __name__ == __main__:
Test().
unittest / pyunit
Unittest, también llamado pyunit, forma parte de una familia de herramientas conocida colectivamente como xunit, un conjunto de frameworks basados en el software sunit para smalltalk, creado por kent Beck, uno de los padres de la extreme programming. Otros ejemplos de herramientas que forman parte de esta familia son junit para java, creada por el propio kent bek junto a Erich gamma, o nunit, para Net.
El uso de unittest es muy sencillo. Para cada grupo de pruebas tenemos que crear una clase que herede de unittest. Testcase, y añadir una serie de métodos que comiencen con test, que serán cada una de las pruebas que queremos ejecutar dentro de esa batería de pruebas.
Para ejecutar las pruebas, basta llamar a la función main() del módulo, con lo que se ejecutaran todos los métodos cuyo nombre comience con test, en orden alfanumérico. Al ejecutar cada una de las pruebas el resultado puede ser:
Ok : la prueba ha pasado con éxito.
Fail : la prueba no ha pasado con éxito. Se lanza una excepción assertionerror para indicarlo.
Error : al ejecutar la prueba se lanzó una excepción distinta de assertionerror.
En el siguiente ejemplo, dado que el método que modela nuestra prueba no lanza ninguna excepción, la prueba pasaría con éxito.
Cita:
Import unittest.
Class ejemplopruebas(unittest. Testcase):
Def test(self):
Pass.
If __name__ == __main__:
Unittest, main().
En este otro, sin embargo, fallaría.
Cita:
Import unittest.
Class ejemplopruebas(unittest. Testcase):
Def test(self):
Raise assertionerror()
If __name__ == __main__:
Unittest, main().
Nada nos impide utilizar cláusulas if para evaluar las condiciones que nos interesen y lanzar una excepción de tipo assertionerror cuando no sea así, pero la clase testcase cuenta con varios métodos que nos pueden facilitar la tarea de realizar comprobaciones sencillas. Son los siguientes:
Assertalmostequal(first, second, places=7, msg=none)o : comprueba que los objetos pasados como parámetros sean iguales hasta el séptimo decimal (o el número de decimales indicado por places).
Assertequal(first, second, msg=none)o : comprueba que los objetos pasados como parámetros sean iguales, assertfalse (expr, msg=none)o : comprueba que la expresión sea falsa.
Assertnotalmostequal(first, second, places=7, msg=none)o : comprueba que los objetos pasados como parámetros no sean iguales hasta el séptimo decimal (o hasta el número de decimales indicado por places).
Assertnotequal(first, second, msg=none)o : comprueba que los objetos pasados como parámetros no sean iguales.
Assertraises(excclass, callábleobj, *args, **kwargs)o : comprueba que al llamar al objeto callábleobj con los parámetros.
Definidos por *args y **kwargs se lanza una excepción de tipo excclass, asserttrue (expr, msg=none)o : comprueba que la expresión sea cierta.
Assert_(expr, msg=none)o : comprueba que la expresión sea cierta, fail(msg=none)o : falla inmediatamente.
Failif(expr, msg=none)o : falla si la expresión es cierta, failifalmostequal(first, second, places=7, msg=none)o : falla si los objetos pasados como parámetros son iguales hasta el séptimo.
Decimal (o hasta el número de decimales indicado por places).
Failifequal(first, second, msg=none)o : falla si los objetos pasados.
Como parámetros son iguales.
Failunless(expr, msg=none)o : falla a menos que la expresión sea cierta.
Failunlessalmostequal(first, second, places=7, msg=none)o : falla a menos que los objetos pasados como parámetros sean iguales hasta el séptimo decimal (o hasta el número de decimales indicado por places).
Failunlessequal(first, second, msg=none)o : falla a menos que los objetos pasados como parámetros sean iguales.
Failunlessraises(excclass, callábleobj, *args, **kwargs)o : falla a menos que al llamar al objeto callábleobj con los parámetros.
Definidos por *args y **kwargs se lance una excepción de tipo excclass.
Como vemos todos los métodos cuentan con un parámetro opcional msg con un mensaje a mostrar cuando dicha comprobación falle.
Retomemos nuestra función para calcular el cuadrado de un número. Para probar el funcionamiento de la función podríamos hacer, por ejemplo, algo así.
Cita:
Import unittest.
Def cuadrado (num):
Calcula el cuadrado de un número.
Return num ** 2
Class ejemplopruebas(unittest. Testcase):
Def test(self):
L = [0, 1, 2, 3]
R = [cuadrado (n) for n in l]
Self, assertequal(r, [0, 1, 4, 9])
If __name__ == __main__:
Unittest, main().
preparación del contexto
En ocasiones es necesario preparar el entorno en el que queremos que se ejecuten las pruebas. Por ejemplo, puede ser necesario introducir unos valores por defecto en una base de datos, crear una conexión con una máquina, crear algún archivo, etc. Esto es lo que se conoce en el mundo de xunit como test fixture.
La clase testcase proporciona un par de métodos que podemos sobrescribir para construir y desconstruir el entorno y que se ejecutan antes y después de las pruebas definidas en esa clase. Estos métodos son setup() y teardown().
Cita:
Class ejemplofixture (unittest. Testcase):
Def setup(self):
Print preparando contexto
Self, lista = [0, 1, 2, 3]
Def test(self):
Print ejecutando prueba
R = [cuadrado (n) for n in self, lista]
Self, assertequal(r, [0, 1, 4, 9])
Def teardown(self):
Print desconstruyendo contexto
Del self, lista.