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:
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.
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.
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.
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.
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.
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.
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.
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.
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>
.
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
.
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.
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.
"""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.
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.
"""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.
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.
===============================
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.
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.
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.
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
.
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í.
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.
_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.