Fuente: www.flaticon.com |
Afortunadamente, existen las pruebas unitarias automatizadas, las cuales son porciones de código diseñadas para comprobar que el código principal está funcionando de la manera esperada.
Veamos un ejemplo. Supongamos que deseamos escribir en Python una función que calcule la hipotenusa c de un triángulo rectángulo dados sus catetos a y b:
El teorema de Pitágoras establece que el cuadrado de la hipotenusa es igual a la suma de los cuadrados de las catetos respectivos, es decir: $$ c^2 = a^2 + b^2 $$ $$ c = \sqrt{a^2 + b^2} $$ En Python la función
hipotenusa
podría quedar codificada de la siguiente manera:from math import sqrt def hipotenusa(a, b): return sqrt(a ** 2 + b ** 2)NOTA: Todo el código que aquí se presenta fue probado con Python 3.7.
El siguiente código pudiera considerarse una prueba unitaria encargada de verificar que la función
hipotenusa
devuelva el resultado esperado a partir de las entradas 3 y 4:if hipotenusa(3.0, 4.0) == 5.0: print('Pasó la prueba') else: print('Falló la prueba')Sin embargo, escribir pruebas similares al ejemplo anterior no es recomendable dado que requiere teclear mucho código cuando deseamos elaborar múltiples pruebas y además no es la manera convencional de hacerlo.
Fuente: searchengineland.com |
La distribución estándar de Python provee dos mecanismos que simplifican la escritura y automatización de nuestras pruebas unitarias:
unittest
y doctest
. La primera opción es un módulo inspirado en los frameworks de pruebas unitarias desarrollados por Erich Gamma y Kent Beck para los lenguajes Java y Smalltalk. La segunda opción, doctest
, generalmente se considera más sencilla de usar que unittest
, aunque esta última puede ser más adecuada para pruebas más complejas.A continuación realizaré una breve introducción al módulo de
doctest
con el fin de elaborar pruebas unitarias al momento de definir funciones en Python.El doctest va dentro del docstring
Un docstring es una cadena de caracteres que se coloca como primer enunciado de un módulo, clase, método o función, con el fin de explicar su intención. Esto lo expliqué hace algunos años con más lujo de detalle en la entrada de este blog titulada Documentando programas en Python.La función
hipotenusa
, definida anteriormente, podría tener el docstring que se muestra aquí:from math import sqrt def hipotenusa(a, b): '''Calcula la hipotenusa de un triángulo rectángulo. Utiliza el teorema de Pitágoras para determinar la hipotenusa a partir de los catetos de un triángulo. Parámetros: a -- primer cateto b -- segundo cateto ''' return sqrt(a ** 2 + b ** 2)Aquí el docstring es una cadena de caracteres multi-líneas, la cual comienza y termina con triples comillas sencillas (
'''
) o dobles ("""
).El módulo
doctest
busca en los docstrings fragmentos de texto que parezcan sesiones interactivas de Python con el fin de ejecutarlos y verificar que funcionan exactamente como se muestran.Por ejemplo, la siguiente sesión interactiva de Python muestra la manera en que se comporta la función
hipotenusa
con diferentes argumentos:>>> hipotenusa(3.0, 4.0) 5.0 >>> hipotenusa(0.0, 0.0) 0.0 >>> hipotenusa(8.0, 15.0) 17.0 >>> hipotenusa(39.0, 80.0) 89.0 >>> round(hipotenusa(1.0, 1.0), 4) 1.4142NOTA: En el último caso se usó la función
round
para redondear el resultado a 4 cifras después del punto decimal. Se recomienda hacerlo de esta manera para evitar los problemas de precisión que surgen cuando aparecen números reales con una parte fraccionaria compuesta de muchas cifras.El texto completo de esta sesión interactiva, incluyendo los indicadores (prompts) del intérprete (
>>>
), se coloca dentro del doctring, típicamente al final, aunque en realidad puede ir donde sea:
from math import sqrt def hipotenusa(a, b): '''Calcula la hipotenusa de un triángulo rectángulo. Utiliza el teorema de Pitágoras para determinar la hipotenusa a partir de los catetos de un triángulo. Parámetros: a -- primer cateto b -- segundo cateto Ejemplos de uso: >>> hipotenusa(3.0, 4.0) 5.0 >>> hipotenusa(0.0, 0.0) 0.0 >>> hipotenusa(8.0, 15.0) 17.0 >>> hipotenusa(39.0, 80.0) 89.0 >>> round(hipotenusa(1.0, 1.0), 4) 1.4142 ''' return sqrt(a ** 2 + b ** 2)
Corriendo las pruebas
Existen dos opciones para correr las pruebas. La primera opción consiste en usar la terminal del sistema y ejecutar un comando similar al siguiente (suponiendo que el archivo que contiene nuestro código se llamapitagoras.py
):
python3 -m doctest pitagoras.pyLa opción
-m doctest
le dice al intérprete de Python que ejecute el módulo doctest
. El comando no produce salida alguna en caso de pasar todas las pruebas.Si alguna prueba llega a fallar, entonces se verá un mensaje con la información pertinente. Por ejemplo, si agregamos al doctring el siguiente caso, el cual tiene un resultado incorrecto:
>>> hipotenusa(2.0, 2.0) 2.0al correr nuevamente las pruebas obtenemos un mensaje similar al siguiente:
************************************************************ File "pitagoras.py", line 25, in pitagoras.hipotenusa Failed example: hipotenusa(2.0, 2.0) Expected: 2.0 Got: 2.8284271247461903 ************************************************************ 1 items had failures: 1 of 6 in pitagoras.hipotenusa ***Test Failed*** 1 failures.Se puede añadir también la opción
-v
(habilitar modo verboso o detallado) en la línea de comando al momento de correr nuestras pruebas. En este caso se produce un reporte completo con la información de todas las pruebas, hayan sido exitosas o no:
python3 -m doctest pitagoras.py -vLa salida en este caso sería:
Trying: hipotenusa(3.0, 4.0) Expecting: 5.0 ok Trying: hipotenusa(0.0, 0.0) Expecting: 0.0 ok Trying: hipotenusa(8.0, 15.0) Expecting: 17.0 ok Trying: hipotenusa(39.0, 80.0) Expecting: 89.0 ok Trying: round(hipotenusa(1.0, 1.0), 4) Expecting: 1.4142 ok Trying: hipotenusa(2.0, 2.0) Expecting: 2.0 ************************************************************ File "pitagoras.py", line 25, in pitagoras.hipotenusa Failed example: hipotenusa(2.0, 2.0) Expected: 2.0 Got: 2.8284271247461903 1 items had no tests: pitagoras ************************************************************ 1 items had failures: 1 of 6 in pitagoras.hipotenusa 6 tests in 2 items. 5 passed and 1 failed. ***Test Failed*** 1 failures.La segunda opción para correr las pruebas consiste en agregar las siguientes tres líneas de código al final del archivo
pitagoras.py
:
if __name__ == '__main__': import doctest doctest.testmod()La instrucción
if
anterior revisa si el archivo actual se está ejecutando como un programa (cuando la variable __name__
es igual a la cadena '__main__'
) o si se importó como un módulo (cuando la variable __name__
es igual al nombre del módulo, en este caso la cadena 'pitagoras'
). Solo deseamos llevar a cabo las pruebas cuando el archivo se corre como un programa y, cuando es así, realizamos la importación del módulo doctest
seguido de la invocación de su función testmod
.Después de lo anterior podemos correr nuestro archivo como nos resulte más conveniente (por ejemplo, desde la terminal o usando nuestro ambiente de desarrollo predilecto). Si es desde la terminal, basta teclear el siguiente comando:
python3 pitagoras.pyTal como vimos anteriormente, si no se produce salida alguna quiere decir que se pasaron todas las pruebas.
Si queremos ver todo el reporte de pruebas exitosas y fallidas podemos añadir nuevamente la opción
-v
en el comando de la terminal:
python3 pitagoras.py -vAlternativamente, el mismo efecto se puede lograr modificando el código para añadir el parámetro opcional
verbose
con valor de verdadero (True
) al momento de invocar la función testmod
:
doctest.testmod(verbose=True)
Ahora, cada vez que el archivo se corra como un programa, sin importar si se utilizó o no la opción -v
desde la terminal, se obtendrá el reporte completo con el resultado de todas las pruebas.Con todas las adecuaciones mencionadas, el archivo
pitagoras.py
queda en su versión final así:
from math import sqrt def hipotenusa(a, b): '''Calcula la hipotenusa de un triángulo rectángulo. Utiliza el teorema de Pitágoras para determinar la hipotenusa a partir de los catetos de un triángulo. Parámetros: a -- primer cateto b -- segundo cateto Ejemplos de uso: >>> hipotenusa(3.0, 4.0) 5.0 >>> hipotenusa(0.0, 0.0) 0.0 >>> hipotenusa(8.0, 15.0) 17.0 >>> hipotenusa(39.0, 80.0) 89.0 >>> round(hipotenusa(1.0, 1.0), 4) 1.4142 >>> hipotenusa(2.0, 2.0) 2.0 ''' return sqrt(a ** 2 + b ** 2) if __name__ == '__main__': import doctest doctest.testmod(verbose=True)Este versión del programa se puede correr desde la terminal, tal como ya se explicó, o desde cualquier editor o ambiente de desarrollo para Python (IDLE, Spyder, Visual Studio Code, etc.) utilizando los respectivos mecanismos disponibles para ejecutar código.
Mejores prácticas
Estas son algunas prácticas recomendadas al momento de escribir pruebas unitarias usandodoctest:
- Deben ser lógicamente lo más simples que se pueda.
- Deben requerir muy poco tiempo para ejecutarse, de preferencia solo unos cuantos milisegundos. Si las pruebas toman mucho tiempo la gente optará por no correrlas.
- Deben procurar abarcar todos los posibles caminos de ejecución del código. Por ejemplo, para cada instrucción
if
debe haber al menos un caso para probar cuando la condición es verdadera y otro caso para cuando la condición es falsa. - Todas las funciones, módulos, clases y métodos deben tener pruebas unitarias apropiadas.
- Incluir casos de prueba para las situaciones de error en donde se lancen excepciones.
reciproco
, la cual obtiene el inverso multiplicativo de su argumento:def reciproco(x): return 1 / xUna sesión interactiva de Python utilizando esta función se muestra a continuación:
>>> reciproco(2) 0.5 >>> reciproco(4) 0.25 >>> reciproco(1) 1.0 >>> reciproco(0) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "otro_ejemplo.py", line 2, in reciproco return 1 / x ZeroDivisionError: division by zeroLa última expresión genera una excepción debido a que Python no permite dividir entre cero.
A partir de la salida de arriba podemos escribir el
doctest
para la función reciproco
. Del mensaje de error solo es necesario conservar dos líneas: la primera (la que comienza con Traceback
) y la última (la que contiene el nombre de la excepción y la explicación del error). Por claridad se recomienda eliminar las demás líneas del stack trace.El
docstring
completo dentro de su función quedaría así:
def reciproco(x): '''Calcula el inverso multiplicativo de x. Se cumple que: x * reciproco(x) == 1.0 Ejemplo de uso: >>> reciproco(2) 0.5 >>> reciproco(4) 0.25 >>> reciproco(1) 1.0 >>> reciproco(0) Traceback (most recent call last): ZeroDivisionError: division by zero ''' return 1 / xPara correr estas pruebas podemos usar cualquiera de las opciones explicadas anteriormente.
Fuente: www.iconarchive.com |
Reflexiones finales
¿Qué beneficios ofrece la automatización de las pruebas unitarias? Según quintagroup, algunos ventajas son:- Detección temprana de problemas. Las pruebas unitarias permiten encontrar y eliminar errores de manera rápida y oportuna. Es bien sabido que un defecto de software es menos costoso entre más pronto se detecte y elimine.
- Mitigación del cambio. Se tiene una certeza razonable de que nuestro programa funciona de cierta manera aún después de hacerle modificaciones, ya sea para corregir errores o añadir funcionalidad nueva.
- Simplificación de la integración. La integración de los diferentes componentes de una aplicación es más sencilla si primero se prueban éstos por separado.
- Mejor documentación. Un programador que desea aprender qué funcionalidad proporciona una unidad y cómo usarla puede revisar las pruebas correspondientes para obtener una comprensión básica de su API.
Fuente: openclipart.org |
Por último, es importante mencionar que la principal limitación de las pruebas unitarias es que no se pueden utilizar para detectar todos los errores en un programa, ya que es imposible evaluar todas sus rutas de ejecución para todos los posibles valores que pueden tomar las variables involucradas, salvo quizás en casos muy triviales. Las pruebas unitarias solo pueden mostrar la presencia o ausencia de errores particulares.
En resumen, las pruebas unitarias son una buena herramienta para facilitar el desarrollo de software de calidad, pero definitivamente no son una bala de plata.
Fuente: magazine.joomla.org |