doctest — Pruebas a través de la documentación

Propósito:Escribir pruebas automatizadas como parte de la documentación de un módulo.

doctest prueba el archivo fuente ejecutando ejemplos integrados en la documentación y verificando que producen los resultados esperados. Funciona analizando el texto de ayuda para encontrar ejemplos, ejecutándolos y luego comparando el texto de salida con el valor esperado. Muchos desarrolladores encuentran que doctest es más fácil de usar que unittest porque, en su forma más simple, no hay interfaz de programación para aprender antes de usarlo. Sin embargo, a medida que los ejemplos se vuelven más complejos, la falta de administración de accesorios puede hacer que escribir pruebas doctest sea más engorroso que usar unittest.

Empezar

El primer paso para configurar doctests es usar el intérprete interactivo para crear ejemplos y luego copiarlos y pegarlos en las cadenas de documentos en el módulo. Aquí, my_function() tiene dos ejemplos dados:

doctest_simple.py
def my_function(a, b):
    """
    >>> my_function(2, 3)
    6
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

Para ejecutar las pruebas, usa doctest como el programa principal a través de la opción -m. Por lo general, no se produce ningún resultado mientras se ejecutan las pruebas, por lo que el siguiente ejemplo incluye la opción -v para que el resultado sea más detallado.

$ python3 -m doctest -v doctest_simple.py

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_simple
1 items passed all tests:
   2 tests in doctest_simple.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

Los ejemplos generalmente no pueden sostenerse por sí solos como explicaciones de una función, por lo que doctest también permite el texto circundante. Busca líneas que comiencen con el mensaje del intérprete (>>>) para encontrar el comienzo de un caso de prueba, y el caso finaliza con una línea en blanco o con el siguiente mensaje del intérprete. El texto intermedio se ignora y puede tener cualquier formato siempre que no se vea como un caso de prueba.

doctest_simple_with_docs.py
def my_function(a, b):
    """Returns a * b.

    Works with numbers:

    >>> my_function(2, 3)
    6

    and strings:

    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

El texto circundante en la cadena de documentación actualizada lo hace más útil para un lector humano. Debido a que es ignorado por doctest, los resultados son los mismos.

$ python3 -m doctest -v doctest_simple_with_docs.py

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_simple_with_docs
1 items passed all tests:
   2 tests in doctest_simple_with_docs.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

Manejo de salida impredecible

Hay otros casos en los que el resultado exacto puede no ser predecible, pero aún debe ser comprobable. Por ejemplo, los valores locales de fecha y hora y los identificadores de objeto cambian en cada ejecución de prueba, la precisión predeterminada utilizada en la representación de valores de coma flotante depende de las opciones del compilador, y las representaciones de cadena de objetos contenedores como los diccionarios pueden no ser deterministas. Aunque estas condiciones no pueden controlarse, existen técnicas para tratarlas.

Por ejemplo, en CPython, los identificadores de objeto se basan en la dirección de memoria de la estructura de datos que contiene el objeto.

doctest_unpredictable.py
class MyClass:
    pass


def unpredictable(obj):
    """Returns a new list containing obj.

    >>> unpredictable(MyClass())
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
    """
    return [obj]

Estos valores de identificación cambian cada vez que se ejecuta un programa, porque se carga en una parte diferente de la memoria.

$ python3 -m doctest -v doctest_unpredictable.py

Trying:
    unpredictable(MyClass())
Expecting:
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
****************************************************************
File ".../doctest_unpredictable.py", line 17, in doctest_unpredi
ctable.unpredictable
Failed example:
    unpredictable(MyClass())
Expected:
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
Got:
    [<doctest_unpredictable.MyClass object at 0x1047a2710>]
2 items had no tests:
    doctest_unpredictable
    doctest_unpredictable.MyClass
****************************************************************
1 items had failures:
   1 of   1 in doctest_unpredictable.unpredictable
1 tests in 3 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

Cuando las pruebas incluyen valores que probablemente cambien de manera impredecible, y donde el valor real no es importante para los resultados de la prueba, usa la opción ELLIPSIS para decirle a doctest que ignore partes del valor de verificación.

doctest_ellipsis.py
class MyClass:
    pass


def unpredictable(obj):
    """Returns a new list containing obj.

    >>> unpredictable(MyClass()) #doctest: +ELLIPSIS
    [<doctest_ellipsis.MyClass object at 0x...>]
    """
    return [obj]

El comentario «#doctest: + ELLIPSIS» después de la llamada de unpredictable() le dice a doctest que active la opción ELLIPSIS para esa prueba. El ... reemplaza la dirección de memoria en la identificación del objeto, por lo que esa parte del valor esperado se ignora y la salida real coincide y la prueba pasa.

$ python3 -m doctest -v doctest_ellipsis.py

Trying:
    unpredictable(MyClass()) #doctest: +ELLIPSIS
Expecting:
    [<doctest_ellipsis.MyClass object at 0x...>]
ok
2 items had no tests:
    doctest_ellipsis
    doctest_ellipsis.MyClass
1 items passed all tests:
   1 tests in doctest_ellipsis.unpredictable
1 tests in 3 items.
1 passed and 0 failed.
Test passed.

Hay casos en los que el valor impredecible no se puede ignorar, ya que eso haría que la prueba sea incompleta o inexacta. Por ejemplo, las pruebas simples se vuelven rápidamente más complejas cuando se trata de tipos de datos cuyas representaciones de cadena son inconsistentes. La forma de cadena de un diccionario, por ejemplo, puede cambiar según el orden en que se agregan las llaves.

doctest_hashed_values.py
keys = ['a', 'aa', 'aaa']

print('dict:', {k: len(k) for k in keys})
print('set :', set(keys))

Debido a la aleatoriedad del hash y la colisión de llaves, el orden de la lista de llaves internas puede ser diferente para el diccionario cada vez que se ejecuta la secuencia de comandos. Los conjuntos usan el mismo algoritmo de hashing y exhiben el mismo comportamiento.

$ python3 doctest_hashed_values.py

dict: {'aa': 2, 'a': 1, 'aaa': 3}
set : {'aa', 'a', 'aaa'}

$ python3 doctest_hashed_values.py

dict: {'a': 1, 'aa': 2, 'aaa': 3}
set : {'a', 'aa', 'aaa'}

La mejor manera de lidiar con estas posibles discrepancias es crear pruebas que produzcan valores que probablemente no cambien. En el caso de los diccionarios y conjuntos, eso podría significar buscar claves específicas individualmente, generar una lista ordenada del contenido de la estructura de datos o comparar con un valor literal para la igualdad en lugar de depender de la representación de la cadena.

doctest_hashed_values_tests.py
import collections


def group_by_length(words):
    """Returns a dictionary grouping words into sets by length.

    >>> grouped = group_by_length([ 'python', 'module', 'of',
    ... 'the', 'week' ])
    >>> grouped == { 2:set(['of']),
    ...              3:set(['the']),
    ...              4:set(['week']),
    ...              6:set(['python', 'module']),
    ...              }
    True

    """
    d = collections.defaultdict(set)
    for word in words:
        d[len(word)].add(word)
    return d

El único ejemplo en realidad se interpreta como dos pruebas separadas, la primera no espera salida de consola y la segunda espera el resultado booleano de la operación de comparación.

$ python3 -m doctest -v doctest_hashed_values_tests.py

Trying:
    grouped = group_by_length([ 'python', 'module', 'of',
    'the', 'week' ])
Expecting nothing
ok
Trying:
    grouped == { 2:set(['of']),
                 3:set(['the']),
                 4:set(['week']),
                 6:set(['python', 'module']),
                 }
Expecting:
    True
ok
1 items had no tests:
    doctest_hashed_values_tests
1 items passed all tests:
   2 tests in doctest_hashed_values_tests.group_by_length
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

Rastreos

Los rastreos son un caso especial de cambio de datos. Dado que las rutas en un rastreo dependen de la ubicación donde se instala un módulo en el sistema de archivos, sería imposible escribir pruebas portátiles si se trataran de la misma manera que otra salida.

doctest_tracebacks.py
def this_raises():
    """This function always raises an exception.

    >>> this_raises()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/no/such/path/doctest_tracebacks.py", line 14, in
      this_raises
        raise RuntimeError('here is the error')
    RuntimeError: here is the error
    """
    raise RuntimeError('here is the error')

doctest hace un esfuerzo especial para reconocer los rastreos e ignorar las partes que pueden cambiar de un sistema a otro.

$ python3 -m doctest -v doctest_tracebacks.py

Trying:
    this_raises()
Expecting:
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/no/such/path/doctest_tracebacks.py", line 14, in
      this_raises
        raise RuntimeError('here is the error')
    RuntimeError: here is the error
ok
1 items had no tests:
    doctest_tracebacks
1 items passed all tests:
   1 tests in doctest_tracebacks.this_raises
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

De hecho, todo el cuerpo del rastreo se ignora y se puede omitir.

doctest_tracebacks_no_body.py
def this_raises():
    """This function always raises an exception.

    >>> this_raises()
    Traceback (most recent call last):
    RuntimeError: here is the error

    >>> this_raises()
    Traceback (innermost last):
    RuntimeError: here is the error
    """
    raise RuntimeError('here is the error')

Cuando doctest ve una línea de encabezado de rastreo (ya sea «Traceback (most recent call last):» o «Traceback (innermost last):», para admitir diferentes versiones de Python), salta hacia adelante para encontrar el tipo de excepción y el mensaje, ignorando por completo las líneas intermedias.

$ python3 -m doctest -v doctest_tracebacks_no_body.py

Trying:
    this_raises()
Expecting:
    Traceback (most recent call last):
    RuntimeError: here is the error
ok
Trying:
    this_raises()
Expecting:
    Traceback (innermost last):
    RuntimeError: here is the error
ok
1 items had no tests:
    doctest_tracebacks_no_body
1 items passed all tests:
   2 tests in doctest_tracebacks_no_body.this_raises
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

Trabajar alrededor de espacios en blanco

En las aplicaciones del mundo real, la salida generalmente incluye espacios en blanco, como líneas en blanco, tabuladores y espacios adicionales para que sea más legible. Las líneas en blanco, en particular, causan problemas con doctest porque se usan para delimitar las pruebas.

doctest_blankline_fail.py
def double_space(lines):
    """Prints a list of lines double-spaced.

    >>> double_space(['Line one.', 'Line two.'])
    Line one.

    Line two.

    """
    for l in lines:
        print(l)
        print()

double_space() toma una lista de líneas de entrada, y las imprime a doble espacio con líneas en blanco entre ellas.

$ python3 -m doctest -v doctest_blankline_fail.py

Trying:
    double_space(['Line one.', 'Line two.'])
Expecting:
    Line one.
****************************************************************
File ".../doctest_blankline_fail.py", line 12, in doctest_blankl
ine_fail.double_space
Failed example:
    double_space(['Line one.', 'Line two.'])
Expected:
    Line one.
Got:
    Line one.
    <BLANKLINE>
    Line two.
    <BLANKLINE>
1 items had no tests:
    doctest_blankline_fail
****************************************************************
1 items had failures:
   1 of   1 in doctest_blankline_fail.double_space
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

La prueba falla, porque interpreta la línea en blanco después de la línea que contiene Line one. en la cadena de documentos como el final de la salida de muestra. Para hacer coincidir las líneas en blanco, reemplázalas en la entrada de muestra con la cadena <BLANKLINE>.

doctest_blankline.py
def double_space(lines):
    """Prints a list of lines double-spaced.

    >>> double_space(['Line one.', 'Line two.'])
    Line one.
    <BLANKLINE>
    Line two.
    <BLANKLINE>
    """
    for l in lines:
        print(l)
        print()

doctest reemplaza las líneas en blanco con el mismo literal antes de realizar la comparación, por lo que ahora los valores reales y esperados coinciden y la prueba pasa.

$ python3 -m doctest -v doctest_blankline.py

Trying:
    double_space(['Line one.', 'Line two.'])
Expecting:
    Line one.
    <BLANKLINE>
    Line two.
    <BLANKLINE>
ok
1 items had no tests:
    doctest_blankline
1 items passed all tests:
   1 tests in doctest_blankline.double_space
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Los espacios en blanco dentro de una línea también pueden causar problemas difíciles con las pruebas. Este ejemplo tiene un solo espacio extra después del 6.

doctest_extra_space.py
def my_function(a, b):
    """
    >>> my_function(2, 3)
    6 
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

Los espacios adicionales pueden encontrar su camino en el código a través de errores de copiar y pegar, pero dado que aparecen al final de la línea, pueden pasar desapercibidos en el archivo fuente y ser invisibles en el informe de falla de la prueba también.

$ python3 -m doctest -v doctest_extra_space.py

Trying:
    my_function(2, 3)
Expecting:
    6
****************************************************************
File ".../doctest_extra_space.py", line 15, in doctest_extra_spa
ce.my_function
Failed example:
    my_function(2, 3)
Expected:
    6
Got:
    6
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_extra_space
****************************************************************
1 items had failures:
   1 of   2 in doctest_extra_space.my_function
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

El uso de una de las opciones de informes basados en diferencias, como REPORT_NDIFF, muestra la diferencia entre los valores reales y esperados con más detalle, y el espacio adicional se hace visible.

doctest_ndiff.py
def my_function(a, b):
    """
    >>> my_function(2, 3) #doctest: +REPORT_NDIFF
    6 
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b

Las diferencias unificadas (REPORT_UDIFF) y de contexto (REPORT_CDIFF) también están disponibles, para la salida donde esos formatos son más legibles.

$ python3 -m doctest -v doctest_ndiff.py

Trying:
    my_function(2, 3) #doctest: +REPORT_NDIFF
Expecting:
    6
****************************************************************
File ".../doctest_ndiff.py", line 16, in doctest_ndiff.my_functi
on
Failed example:
    my_function(2, 3) #doctest: +REPORT_NDIFF
Differences (ndiff with -expected +actual):
    - 6
    ?  -
    + 6
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_ndiff
****************************************************************
1 items had failures:
   1 of   2 in doctest_ndiff.my_function
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

Hay casos en los que es beneficioso agregar espacio en blanco adicional en la salida de muestra para la prueba, y hacer que doctest lo ignore. Por ejemplo, las estructuras de datos pueden ser más fáciles de leer cuando se extienden a través de varias líneas, incluso si su representación cabe en una sola línea.


def my_function(a, b):
    """Returns a * b.

    >>> my_function(['A', 'B'], 3) #doctest: +NORMALIZE_WHITESPACE
    ['A', 'B',
     'A', 'B',
     'A', 'B']

    This does not match because of the extra space after the [ in
    the list.

    >>> my_function(['A', 'B'], 2) #doctest: +NORMALIZE_WHITESPACE
    [ 'A', 'B',
      'A', 'B', ]
    """
    return a * b

Cuando NORMALIZE_WHITESPACE está activado, cualquier espacio en blanco en los valores reales y esperados se considera una coincidencia. No se puede agregar espacio en blanco al valor esperado donde no existe ninguno en la salida, pero la longitud de la secuencia de espacio en blanco y los caracteres de espacio en blanco reales no necesitan coincidir. El primer ejemplo de prueba hace que esta regla sea correcta y pasa, a pesar de que hay espacios adicionales y nuevas líneas. El segundo tiene espacios en blanco adicionales después de [ y antes de ], por lo que falla.

$ python3 -m doctest -v doctest_normalize_whitespace.py

Trying:
    my_function(['A', 'B'], 3) #doctest: +NORMALIZE_WHITESPACE
Expecting:
    ['A', 'B',
     'A', 'B',
     'A', 'B',]
***************************************************************
File "doctest_normalize_whitespace.py", line 13, in doctest_nor
malize_whitespace.my_function
Failed example:
    my_function(['A', 'B'], 3) #doctest: +NORMALIZE_WHITESPACE
Expected:
    ['A', 'B',
     'A', 'B',
     'A', 'B',]
Got:
    ['A', 'B', 'A', 'B', 'A', 'B']
Trying:
    my_function(['A', 'B'], 2) #doctest: +NORMALIZE_WHITESPACE
Expecting:
    [ 'A', 'B',
      'A', 'B', ]
***************************************************************
File "doctest_normalize_whitespace.py", line 21, in doctest_nor
malize_whitespace.my_function
Failed example:
    my_function(['A', 'B'], 2) #doctest: +NORMALIZE_WHITESPACE
Expected:
    [ 'A', 'B',
      'A', 'B', ]
Got:
    ['A', 'B', 'A', 'B']
1 items had no tests:
    doctest_normalize_whitespace
***************************************************************
1 items had failures:
   2 of   2 in doctest_normalize_whitespace.my_function
2 tests in 2 items.
0 passed and 2 failed.
***Test Failed*** 2 failures.

Lugares de las pruebas

Todas las pruebas en los ejemplos hasta ahora se han escrito en las cadenas de documentos de las funciones que están probando. Esto es conveniente para los usuarios que examinan las cadenas de documentos para obtener ayuda con la función (especialmente con pydoc), pero doctest también busca pruebas en otros lugares. La ubicación obvia para pruebas adicionales es en las cadenas de documentos en otras partes del módulo.

doctest_docstrings.py
"""Tests can appear in any docstring within the module.

Module-level tests cross class and function boundaries.

>>> A('a') == B('b')
False
"""


class A:
    """Simple class.

    >>> A('instance_name').name
    'instance_name'
    """

    def __init__(self, name):
        self.name = name

    def method(self):
        """Returns an unusual value.

        >>> A('name').method()
        'eman'
        """
        return ''.join(reversed(self.name))


class B(A):
    """Another simple class.

    >>> B('different_name').name
    'different_name'
    """

Las cadenas de documentos en los niveles de módulo, clase y función pueden contener pruebas.

$ python3 -m doctest -v doctest_docstrings.py

Trying:
    A('a') == B('b')
Expecting:
    False
ok
Trying:
    A('instance_name').name
Expecting:
    'instance_name'
ok
Trying:
    A('name').method()
Expecting:
    'eman'
ok
Trying:
    B('different_name').name
Expecting:
    'different_name'
ok
1 items had no tests:
    doctest_docstrings.A.__init__
4 items passed all tests:
   1 tests in doctest_docstrings
   1 tests in doctest_docstrings.A
   1 tests in doctest_docstrings.A.method
   1 tests in doctest_docstrings.B
4 tests in 5 items.
4 passed and 0 failed.
Test passed.

Hay casos en los que existen pruebas para un módulo que deben incluirse con el código fuente pero no en el texto de ayuda de un módulo, por lo que deben colocarse en otro lugar que no sean las cadenas de documentos. doctest también busca una variable de nivel de módulo llamada __test__ y la usa para localizar otras pruebas. El valor de __test__ debe ser un diccionario que asigne nombres de conjuntos de pruebas (como cadenas) a cadenas, módulos, clases o funciones.

doctest_private_tests.py
import doctest_private_tests_external

__test__ = {
    'numbers': """
>>> my_function(2, 3)
6

>>> my_function(2.0, 3)
6.0
""",

    'strings': """
>>> my_function('a', 3)
'aaa'

>>> my_function(3, 'a')
'aaa'
""",

    'external': doctest_private_tests_external,
}


def my_function(a, b):
    """Returns a * b
    """
    return a * b

Si el valor asociado con una llave es una cadena, se trata como una cadena de documentos y se escanea en busca de pruebas. Si el valor es una clase o función, doctest los busca recursivamente por cadenas de documentos, que luego se escanean en busca de pruebas. En este ejemplo, el módulo doctest_private_tests_external tiene una sola prueba en su cadena de documentos.

doctest_private_tests_external.py
"""External tests associated with doctest_private_tests.py.

>>> my_function(['A', 'B', 'C'], 2)
['A', 'B', 'C', 'A', 'B', 'C']
"""

Después de escanear el archivo de ejemplo, doctest encuentra un total de cinco pruebas para ejecutar.

$ python3 -m doctest -v doctest_private_tests.py

Trying:
    my_function(['A', 'B', 'C'], 2)
Expecting:
    ['A', 'B', 'C', 'A', 'B', 'C']
ok
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function(2.0, 3)
Expecting:
    6.0
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
Trying:
    my_function(3, 'a')
Expecting:
    'aaa'
ok
2 items had no tests:
    doctest_private_tests
    doctest_private_tests.my_function
3 items passed all tests:
   1 tests in doctest_private_tests.__test__.external
   2 tests in doctest_private_tests.__test__.numbers
   2 tests in doctest_private_tests.__test__.strings
5 tests in 5 items.
5 passed and 0 failed.
Test passed.

Documentación externa

Mezclar las pruebas con el código regular no es la única forma de usar doctest. También se pueden usar ejemplos integrados en archivos de documentación externos del proyecto, como los archivos de reStructuredText.

doctest_in_help.py
def my_function(a, b):
    """Returns a*b
    """
    return a * b

La ayuda para este módulo de muestra se guarda en un archivo separado, doctest_in_help.txt. Los ejemplos que ilustran cómo usar el módulo se incluyen con el texto de ayuda y se puede usar doctest para encontrarlos y ejecutarlos.

doctest_in_help.txt
===============================
 How to Use doctest_in_help.py
===============================

This library is very simple, since it only has one function called
``my_function()``.

Numbers
=======

``my_function()`` returns the product of its arguments.  For numbers,
that value is equivalent to using the ``*`` operator.

::

    >>> from doctest_in_help import my_function
    >>> my_function(2, 3)
    6

It also works with floating-point values.

::

    >>> my_function(2.0, 3)
    6.0

Non-Numbers
===========

Because ``*`` is also defined on data types other than numbers,
``my_function()`` works just as well if one of the arguments is a
string, a list, or a tuple.

::

    >>> my_function('a', 3)
    'aaa'

    >>> my_function(['A', 'B', 'C'], 2)
    ['A', 'B', 'C', 'A', 'B', 'C']

Las pruebas en el archivo de texto se pueden ejecutar desde la línea de comandos, al igual que con los módulos de código fuente de Python.

$ python3 -m doctest -v doctest_in_help.txt

Trying:
    from doctest_in_help import my_function
Expecting nothing
ok
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function(2.0, 3)
Expecting:
    6.0
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
Trying:
    my_function(['A', 'B', 'C'], 2)
Expecting:
    ['A', 'B', 'C', 'A', 'B', 'C']
ok
1 items passed all tests:
   5 tests in doctest_in_help.txt
5 tests in 1 items.
5 passed and 0 failed.
Test passed.

Normalmente, doctest configura el entorno de ejecución de prueba para incluir a los miembros del módulo que se está probando, por lo que las pruebas no necesitan importar el módulo explícitamente. En este caso, sin embargo, las pruebas no están definidas en un módulo de Python, y doctest no sabe cómo configurar el espacio de nombres global, por lo que los ejemplos deben hacer el trabajo de importación ellos mismos. Todas las pruebas en un archivo dado comparten el mismo contexto de ejecución, por lo que importar el módulo una vez en la parte superior del archivo es suficiente.

Ejecutar las pruebas

Todos los ejemplos anteriores usan el corredor de prueba de línea de comando integrado en doctest. Es fácil y conveniente para un solo módulo, pero rápidamente se volverá tedioso a medida que un paquete se extienda en múltiples archivos. Hay varios enfoques alternativos.

Por módulo

Las instrucciones para ejecutar doctest contra el código fuente se pueden incluir en la parte inferior de los módulos.

doctest_testmod.py
def my_function(a, b):
    """
    >>> my_function(2, 3)
    6
    >>> my_function('a', 3)
    'aaa'
    """
    return a * b


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

Llamar a testmod() solo si el nombre del módulo actual es __main__ asegura que las pruebas solo se ejecutan cuando el módulo se invoca como un programa principal.

$ python3 doctest_testmod.py -v

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

El primer argumento para testmod() es un módulo que contiene código para escanear en busca de pruebas. Una secuencia de comandos de prueba separada puede usar esta función para importar el código real y ejecutar las pruebas en cada módulo, uno tras otro.

doctest_testmod_other_module.py
import doctest_simple

if __name__ == '__main__':
    import doctest
    doctest.testmod(doctest_simple)

Se puede construir un conjunto de pruebas para el proyecto importando cada módulo y ejecutando sus pruebas.

$ python3 doctest_testmod_other_module.py -v

Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
1 items had no tests:
    doctest_simple
1 items passed all tests:
   2 tests in doctest_simple.my_function
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

Por archivo

testfile() funciona de manera similar a testmod(), permitiendo que las pruebas se invoquen explícitamente en un archivo externo desde el programa de prueba.

doctest_testfile.py
import doctest

if __name__ == '__main__':
    doctest.testfile('doctest_in_help.txt')

Tanto testmod() como testfile() incluyen parámetros opcionales para controlar el comportamiento de las pruebas a través de las opciones doctest. Consulta la documentación de la biblioteca estándar para obtener más detalles sobre esas características, la mayoría de las veces no son necesarias.

$ python3 doctest_testfile.py -v

Trying:
    from doctest_in_help import my_function
Expecting nothing
ok
Trying:
    my_function(2, 3)
Expecting:
    6
ok
Trying:
    my_function(2.0, 3)
Expecting:
    6.0
ok
Trying:
    my_function('a', 3)
Expecting:
    'aaa'
ok
Trying:
    my_function(['A', 'B', 'C'], 2)
Expecting:
    ['A', 'B', 'C', 'A', 'B', 'C']
ok
1 items passed all tests:
   5 tests in doctest_in_help.txt
5 tests in 1 items.
5 passed and 0 failed.
Test passed.

Conjunto de pruebas unitarias

Cuando ambos unittest y doctest se usan para probar el mismo código en diferentes situaciones, la integración de unittest en doctest se puede usar para ejecutar las pruebas juntas. Dos clases, DocTestSuite y DocFileSuite crean conjuntos de pruebas compatibles con la interfaz de programación del corredor de pruebas unittest.

doctest_unittest.py
import doctest
import unittest

import doctest_simple

suite = unittest.TestSuite()
suite.addTest(doctest.DocTestSuite(doctest_simple))
suite.addTest(doctest.DocFileSuite('doctest_in_help.txt'))

runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

Las pruebas de cada archivo fuente se agrupan en un solo resultado, en lugar de informarse individualmente.

$ python3 doctest_unittest.py

my_function (doctest_simple)
Doctest: doctest_simple.my_function ... ok
doctest_in_help.txt
Doctest: doctest_in_help.txt ... ok

----------------------------------------------------------------
Ran 2 tests in 0.003s

OK

Contexto de las pruebas

El contexto de ejecución creado por doctest mientras ejecuta pruebas contiene una copia de las variables globales de nivel de módulo para el módulo de prueba. Cada fuente de prueba (función, clase, módulo) tiene su propio conjunto de valores globales para aislar un poco las pruebas entre sí, por lo que es menos probable que interfieran entre sí.

doctest_test_globals.py
class TestGlobals:

    def one(self):
        """
        >>> var = 'value'
        >>> 'var' in globals()
        True
        """

    def two(self):
        """
        >>> 'var' in globals()
        False
        """

TestGlobals tiene dos métodos: one() y two(). Las pruebas en la cadena de documentación para one() establecen una variable global, y la prueba para two() la busca (esperando no encontrarla).

$ python3 -m doctest -v doctest_test_globals.py

Trying:
    var = 'value'
Expecting nothing
ok
Trying:
    'var' in globals()
Expecting:
    True
ok
Trying:
    'var' in globals()
Expecting:
    False
ok
2 items had no tests:
    doctest_test_globals
    doctest_test_globals.TestGlobals
2 items passed all tests:
   2 tests in doctest_test_globals.TestGlobals.one
   1 tests in doctest_test_globals.TestGlobals.two
3 tests in 4 items.
3 passed and 0 failed.
Test passed.

Sin embargo, eso no significa que las pruebas no puedan interferir entre sí si cambian el contenido de las variables mutables definidas en el módulo.

doctest_mutable_globals.py
_module_data = {}


class TestGlobals:

    def one(self):
        """
        >>> TestGlobals().one()
        >>> 'var' in _module_data
        True
        """
        _module_data['var'] = 'value'

    def two(self):
        """
        >>> 'var' in _module_data
        False
        """

La variable de módulo _module_data se cambia por las pruebas para one(), lo que hace que la prueba para two() falle.

$ python3 -m doctest -v doctest_mutable_globals.py

Trying:
    TestGlobals().one()
Expecting nothing
ok
Trying:
    'var' in _module_data
Expecting:
    True
ok
Trying:
    'var' in _module_data
Expecting:
    False
****************************************************************
File ".../doctest_mutable_globals.py", line 25, in doctest_mutab
le_globals.TestGlobals.two
Failed example:
    'var' in _module_data
Expected:
    False
Got:
    True
2 items had no tests:
    doctest_mutable_globals
    doctest_mutable_globals.TestGlobals
1 items passed all tests:
   2 tests in doctest_mutable_globals.TestGlobals.one
****************************************************************
1 items had failures:
   1 of   1 in doctest_mutable_globals.TestGlobals.two
3 tests in 4 items.
2 passed and 1 failed.
***Test Failed*** 1 failures.

Si se necesitan valores globales para las pruebas, para parametrizarlos para un entorno, por ejemplo, los valores se pueden pasar a testmod() y testfile() para configurar el contexto utilizando los datos controlados por el que llama.

Ver también

  • Documentación de la biblioteca estándar para doctest
  • The Mighty Dictionary – Presentación de Brandon Rhodes en PyCon 2010 sobre las operaciones internas de operations of the dict.
  • difflib – Biblioteca de cálculo de diferencia de secuencia de Python, utilizada para producir la salida ndiff.
  • Sphinx – Además de ser la herramienta de procesamiento de documentación para la biblioteca estándar de Python, Sphinx ha sido adoptado por muchos proyectos de terceros porque es fácil de usar y produce resultados limpios en varios formatos digitales e impresos. Sphinx incluye una extensión para ejecutar doctests, ya que procesa los archivos fuente de la documentación, por lo que los ejemplos son siempre precisos.
  • py.test – Corredor de pruebas de terceros con soporte doctest.
  • nose2 – Corredor de pruebas de terceros con soporte doctest.
  • Manuel – Corredor de pruebas de terceros, basadas en la documentación con extracción de casos de prueba más avanzada e integración con Sphinx.