Saltar a contenido
 No os perd谩is mi futuro contenido, seguidme en y Youtube 馃榿

Doctest

Si por un lado las docstrings nos permit铆an describir documentaci贸n, los doctest nos permiten combinar pruebas en la propia documentaci贸n.

Este concepto de integrar las pruebas en la documentaci贸n nos ayuda a mantener las pruebas actualizadas, y adem谩s nos sirve como ejemplo de utilizaci贸n del c贸digo, ayud谩ndonos a explicar su prop贸sito.

Para utilizar doctests hay que inidicar una l铆nea dentro de la documentaci贸n de la siguiente forma:

Note

>>>

De esta Python entender谩 que debe ejecutar el contenido dentro del comentario como si fuera c贸digo normal, y lo har谩 hasta que encuentre una l铆nea en blanco (o llegue al final de la documentaci贸n).

La mejor forma de ver a doctest en acci贸n.

Definiendo pruebas

Por regla general cada prueba va ligada a una funcionalidad, pueden ser funciones, clases o sus m茅todos. Por ejemplo, dada una funci贸n suma...

Note

def suma(a, b):
    """Esta funci贸n recibe dos par谩metros y devuelve la suma de ambos"""
    return a+b

Para realizar una prueba dentro de la funci贸n, vamos a ejecutar un c贸digo de prueba de la propia suma:

Note

def suma(a, b):
    """Esta funci贸n recibe dos par谩metros y devuelve la suma de ambos

    >>> suma(5,10)
    """
    return a+b

Bien, ya tenemos la prueba, pero ahora nos falta indicarle a doctest cu谩l es el resultado que deber铆a devolver la prueba, y eso lo indicaremos en la siguiente l铆nea:

Note

def suma(a, b):
    """Esta funci贸n recibe dos par谩metros y devuelve la suma de ambos

    >>> suma(5,10)
    15
    """
    return a+b

隆Muy bien! Ahora tenemos que ejecutar la pruebas y ver si funciona o no, pero antes tenemos que adaptar el c贸digo.

Pruebas en un m贸dulo

Para ejecutar pruebas tendremos que utilizar la terminal, as铆 vamos a guardar la funci贸n en un script test.py como si fuera un m贸dulo con funciones.

Ahora justo al final indicaremos que se ejecten las pruebas doctest de las funciones del m贸dulo escribiendo el siguiente c贸digo abajo del todo:

Note

import doctest
doctest.testmod()  # Notar que mod significa m贸dulo

Esto ser铆a suficiente, pero con el objetivo de evitar que este c贸digo se ejecute al importarlo desde otro lugar, se suele indicar de la siguiente forma:

Note

if __name__ == "__main__":
    import doctest
    doctest.testmod()

As铆 煤nicamente se lanzar谩n las pruebas al ejecutar directamente el m贸dulo, y ya podremos ejecutar el m贸dulo desde la terminal:

Note

python test.py

Como resultado veremos que no se muestra nada. Eso no significa que no se ejecute nuestra prueba, sino que esta ha funcionado correctamente y no hay fallos.

Si queremos podemos mostrar todo el registro de ejecuci贸n pasando un argumento -v a python justo al final:

Note

python test.py -v

Y entonces veremos el siguiente resultado:

Note

Trying:
    suma(5,10)
Expecting:
    15
ok
1 items had no tests:
    __main__
1 items passed all tests:
1 tests in __main__.suma
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

En el que se prueba el c贸digo suma(5,10), se espera 15 y el resultado es ok; un resumen y finalmente Test passed.

Creando varias pruebas

Evidentemente podemos definir m煤ltiples pruebas. Probemos tambi茅n alguna que sabemos que es incorrecta:

Note

def suma(a, b):
    """Esta funci贸n recibe dos par谩metros y devuelve la suma de ambos

    >>> suma(5,10)
    15

    >>> suma(0,0)
    1

    >>> suma(-5,7)
    2
    """
    return a+b

Ahora, si ejecutamos el script de forma normal...

Note

python test.py

A diferencia que antes s铆 que nos muestra algo, indic谩ndonos que uno de los tests a fallado:

Note

**********************************************************************
File "test.py", line 7, in __main__.suma
Failed example:
    suma(0,0)
Expected:
    1
Got:
    0
**********************************************************************
1 items had failures:
1 of   3 in __main__.suma
***Test Failed*** 1 failures.

La cuesti贸n ahora ser铆a revisar el test si es incorrecto, o adaptar la funci贸n para que devuelve el resultado esperado en el test. Evidentemente en este caso hemos hecho un test incorrecto a prop贸sito as铆 que simplemente lo borrar铆amos.

Pruebas que nos gu铆an

Una de las ventajas de usar tests es que podemos utilizarlos para detectar posibles fallas. En el siguiente ejemplo vamos a guiarnos de los tests para implementar correctamente una funci贸n.

Note

def palindromo(palabra):
    """
    Comprueba si una palabra es un pal铆ndrimo. Los pal铆ndromos son 
    palabras o frases que se leen igual de ambos lados.
    Si es un pal铆ndromo devuelve True y si no False

    >>> palindromo("radar")
    True

    >>> palindromo("somos")
    True

    >>> palindromo("holah")
    False
    """
    if palabra == palabra[::-1]: 
        return True
    else:
        return False

Bien, 驴pero que ocurre si hacemos el siguiente test?

Note

>>> palindromo("Ana")
True

Pues que nos falla:

Note

**********************************************************************
File "test.py", line 11, in __main__.palindromo
Failed example:
    palindromo("Ana")
Expected:
    True
Got:
    False
**********************************************************************
1 items had failures:
1 of   3 in __main__.palindromo
***Test Failed*** 1 failures.

Claro, es que Ana empieza por may煤scula, pero hay que recordar que un pal铆ndrimo lo es si se pronuncia igual, por tanto las may煤sculas no debes afectar. En este caso por tanto tendr铆amos que readaptar nuestro c贸digo para prevenir el error:

Note

def palindromo(palabra):
    """
    Comprueba si una palabra es un pal铆ndrimo. Los pal铆ndromos son 
    palabras o frases que se leen igual de ambos lados.
    Si es un pal铆ndromo devuelve True y si no False

    >>> palindromo("radar")
    True

    >>> palindromo("somos")
    True

    >>> palindromo("holah")
    False

    >>> palindromo("Ana")
    True
    """
    if palabra.lower() == palabra[::-1].lower(): 
        return True
    else:
        return False

Y en el test de la frase... 驴"Atar a la rata"? Esto tambi茅n es un pal铆ndromo:

Note

>>> palindromo("Atar a la rata")
True

Note

**********************************************************************
Failed example:
    palindromo("Atar a la rata")
Expected:
    True
Got:
    False
**********************************************************************
1 items had failures:
1 of   4 in __main__.palindromo
***Test Failed*** 1 failures.

隆Nos falla de nuevo! El problema en este caso son los espacios, ya que estos no se leen pero si existen en la cadena, la frase no concuerda.

Note

def palindromo(palabra):
    """
    Comprueba si una palabra es un pal铆ndrimo. Los pal铆ndromos son 
    palabras o frases que se leen igual de ambos lados.
    Si es un pal铆ndromo devuelve True y si no False

    >>> palindromo("radar")
    True

    >>> palindromo("somos")
    True

    >>> palindromo("holah")
    False

    >>> palindromo("Atar a la rata")
    True
    """
    if palabra.lower().replace(" ", "") == 
                                palabra[::-1].lower().replace(" ", ""): 
        return True
    else:
        return False

Todav铆a habr铆a algunos casos como los acentos que nos dar铆an fallos, pero ya v茅is por donde va la l贸gica. Se trata de guiarnos a partir de los tests para crear la funci贸n correctamente.

De hecho en el mundo de la programaci贸n hay una pr谩ctica conocida como TDD, Test Driven Development o Desarrollo guiado por pruebas que trata de escribir las pruebas primero y luego refactorizar para ir puliendo la funcionalidad.

Es una pr谩ctica algo avanzada y no la veremos en este curso, pero vale la pena comentarla.

Tests avanzados

Hasta ahora hemos hecho unos tests muy simples, pero los doctests son muy flexibles. Algunas de sus funcionalidades interesantes son la posibilidad de ejecutar bloques de c贸digo o la captura de excepciones.

Para crear un test que incluya un bloque de c贸digo, debemos utilizar las sentencias anidadas para simular tabulaciones:

Note

...

Note

def doblar(lista):
    """Dobla el valor de los elementos de una lista
    >>> l = [1, 2, 3, 4, 5] 
    >>> doblar(l)
    [2, 4, 6, 8, 10]
    """
    return [n*2 for n in lista]

En este caso hemos creado la lista del test manualmente, pero podr铆amos generarla con un bucle utilizando sentencias anidadas:

Note

def doblar(lista):
    """Dobla el valor de los elementos de una lista
    >>> l = [1, 2, 3, 4, 5] 
    >>> doblar(l)
    [2, 4, 6, 8, 10]

    >>> l = [] 
    >>> for i in range(10):
    ...     l.append(i)
    >>> doblar(l)
    [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
    """
    return [n*2 for n in lista]

Si ejecutamos el script monitorizando todo:

Note

python test.py -v

Podemos observar la ejecuci贸n del test avanzado:

Note

Trying:
    l = [1, 2, 3, 4, 5]
Expecting nothing
ok
Trying:
    doblar(l)
Expecting:
    [2, 4, 6, 8, 10]
ok
Trying:
    l = []
Expecting nothing
ok
Trying:
    for i in range(10):
        l.append(i)
Expecting nothing
ok
Trying:
    doblar(l)
Expecting:
    [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
ok
1 items had no tests:
    __main__
1 items passed all tests:
5 tests in __main__.doblar
5 tests in 2 items.
5 passed and 0 failed.
Test passed.

Por 煤ltimo vamos a volver a nuestra funci贸n suma para tratar excepciones dentro de los tests.

Note

def suma(a, b):
    """Esta funci贸n recibe dos par谩metros y devuelve la suma de ambos

    Pueden ser n煤meros:

    >>> suma(5,10)
    15

    >>> suma(-5,7)
    2

    Cadenas de texto:

    >>> suma('aa','bb')
    'aabb'

    O listas:

    >>> a = [1, 2, 3]
    >>> b = [4, 5, 6]
    >>> suma(a,b)
    [1, 2, 3, 4, 5, 6]
    """
    return a+b

Ahora sabemos que no podemos sumar tipos distintos, 驴c贸mo podemos tenerlo en cuenta en un test?

Pues por ahora podemos suponer un resultado y comprobar el resultado cuando falle:

Note

>>> suma(10,"hola")
"10hola"

Note

def suma(a, b):
    """Esta funci贸n recibe dos par谩metros y devuelve la suma de ambos

    Pueden ser n煤meros:

    >>> suma(5,10)
    15

    >>> suma(-5,7)
    2

    Cadenas de texto:

    >>> suma('aa','bb')
    'aabb'

    O listas:

    >>> a = [1, 2, 3]
    >>> b = [4, 5, 6]
    >>> suma(a,b)
    [1, 2, 3, 4, 5, 6]

    Sin embargo no podemos sumar elementos de tipos diferentes:

    >>> suma(10,"hola")
    "10hola"
    """
    return a+b

Si ejecutamos el script monitorizando todo:

Note

python test.py -v

Podemos observar el fallo:

Note

Trying:
    suma(5,10)
Expecting:
    15
ok
Trying:
    suma(-5,7)
Expecting:
    2
ok
Trying:
    suma('aa','bb')
Expecting:
    'aabb'
ok
Trying:
    a = [1, 2, 3]
Expecting nothing
ok
Trying:
    b = [4, 5, 6]
Expecting nothing
ok
Trying:
    suma(a,b)
Expecting:
    [1, 2, 3, 4, 5, 6]
ok
Trying:
    suma(10,"hola")
Expecting:
    "10hola"
**********************************************************************
File "test.py", line 26, in __main__.suma
Failed example:
    suma(10,"hola")
Exception raised:
    Traceback (most recent call last):
    File "C:\Program Files\Anaconda3\lib\doctest.py", line 1321, in __run
        compileflags, 1), test.globs)
    File "<doctest __main__.suma[6]>", line 1, in <module>
        suma(10,"hola")
    File "test.py", line 29, in suma
        return a+b
    TypeError: unsupported operand type(s) for +: 'int' and 'str'
1 items had no tests:
    __main__
**********************************************************************
1 items had failures:
1 of   7 in __main__.suma
7 tests in 2 items.
6 passed and 1 failed.
***Test Failed*** 1 failures.

Concretamente debemos fijarnos en la primera l铆nea y 煤ltima de la excepci贸n:

Note

Traceback (most recent call last):
    ...
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Y precisamente esto es lo que tenemos que indicar en el test:

Note

def suma(a, b):
    """Esta funci贸n recibe dos par谩metros y devuelve la suma de ambos

    Pueden ser n煤meros:

    >>> suma(5,10)
    15

    >>> suma(-5,7)
    2

    Cadenas de texto:

    >>> suma('aa','bb')
    'aabb'

    O listas:

    >>> a = [1, 2, 3]
    >>> b = [4, 5, 6]
    >>> suma(a,b)
    [1, 2, 3, 4, 5, 6]

    Sin embargo no podemos sumar elementos de tipos diferentes:

    >>> suma(10,"hola")
    Traceback (most recent call last):
        ...
    TypeError: unsupported operand type(s) for +: 'int' and 'str'
    """
    return a+b

脷ltima edici贸n: 6 de Octubre de 2018