doctest - Test attraverso la documentazione

Scopo Scrive test automatizzati come parte della documentazione di un modulo
Versione Python 2.1

Il modulo doctest consente di verificare il proprio codice eseguendo esempi incorporati nella documentazione e verificando che essi producano i risultati attesi. Funziona analizzando il testo di aiuto per cercare gli esempi, eseguirli, quindi confrontare il testo in output con il valore che ci si aspetta. Molti sviluppatori trovano doctest più semplice di unittest in quanto, nella sua forma più semplice, non richiede di imparare una API prima di iniziare ad usarlo. Comunque, non appena gli esempi diventano più complessi la mancanza di gestione delle fixture può rendere la scrittura di test con doctest più complicata rispetto ad unittest.

Iniziare

Il primo passe per impostare dei doctest è usare l'interprete interattivo per creare esempi, quindi copiare ed incollarli nelle docstring del proprio modulo. Qui, my_function() viene corredata da due esempi:

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

Per eseguire i test, si usa doctest come programma principale attraverso l'opzione -m dell'interprete. In genere non viene prodotto alcun output mentre i test sono in esecuzione, quindi gli esempi seguenti includono l'opzione -v che rende l'output più dettagliato.

$ python -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.

Gli esempi in genere da soli non bastano come spiegazione di una funzione, quindi doctest consente di mantenere il testo circostante che normalmente si vorrebbe includere nella documentazione. Cerca le righe che iniziano con il prompt dell'interprete >>> per trovare l'inizio di un caso di test Il caso viene concluso da un riga vuota, o da un altro prompt dell'interprete. Il testo che si interpone viene ignorato, e può avere un qualunque formato fintanto che non possa essere scambiato per un caso di test.

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

    Funziona con le cifre:

    >>> my_function(2, 3)
    6

    e le stringhe:

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

Il testo circostante il test nela >docstring aggiornata la rende più utile al lettore, e viene ignorato da doctest, ed il risultato è lo stesso.

$ python -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.

Gestire Output Imprevedibile

Ci sono altri casi dove l'output esatto non è prevedibile, ma dovrebbe comunque essere verificato. Valori di data ed ora localizzate ed identificativi di oggetti cambiano ad ogni esecuzione del test. La precisione predefinita usata nella rappresentazione di valori a virgola mobile dipende dalle opzioni del compilatore. Le rappresentazioni stringa degli oggetti potrebbero essere inaspettate. Sebbene queste condizioni siano al di fuori del proprio controllo, esistono tecniche per affrontarle.

Ad esempio, in CPython, gli identificativi degli oggetti sono basati sull'indirizzo di memoria della struttura dati che tiene l'oggetto.

class MyClass(object):
    pass

def unpredictable(obj):
    """Ritorna una nuova lista che contiene obj.

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

I valori di identificativo variano ogni volta che il programma viene eseguito, in quanto esso viene caricato in parti di memoria diverse.

$ python -m doctest -v doctest_unpredictable.py
Trying:
    unpredictable(MyClass())
Expecting:
    []
**********************************************************************
File "doctest_unpredictable.py", line 10, in doctest_unpredictable.unpredictable
Failed example:
    unpredictable(MyClass())
Expected:
    []
Got:
    []
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.

Quando il test comprende valori che probabilmente cambieranno in modo imprevedibile, e laddove il reale valore non è importanti ai fini del risultato del test, si può usare l'opzione ELLIPSIS per dire a doctest di ignorare delle porzioni del valore da verificare.

class MyClass(object):
    pass

def unpredictable(obj):
    """Ritorna una nuova lista che contiene obj.

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

Il commento dopo la chiamata ad unpredictable() (#doctest: +ELLIPSIS) dice a doctest di attivare l'opzione ELLIPSIS per quel test. Il ... sostituisce l'indirizzo di memoria nell'identificativo dell'oggetto, in modo che quella porzione del valore atteso venga ignorata, l'output effettivo corrisponde ed il test passa.

$ python -m doctest -v doctest_ellipsis.py
Trying:
    unpredictable(MyClass()) #doctest: +ELLIPSIS
Expecting:
    []
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.

Ci sono casi nei quali non si può ignorare un valore imprevedibile, perchè farebbe perdere di significato al test. Ad esempio, dei semplici test diventano velocemente più complessi quando occorre fare fronte a tipi di dato le cui rappresentazioni stringa sono inconsistenti. La forma stringa di un dizionario, ad esempio, potrebbe cambiare in base all'ordine con il quale sono aggiunte le chiavi.

keys = [ 'a', 'aa', 'aaa' ]

d1 = dict( (k,len(k)) for k in keys )
d2 = dict( (k,len(k)) for k in reversed(keys) )

print
print 'd1:', d1
print 'd2:', d2
print 'd1 == d2:', d1 == d2

s1 = set(keys)
s2 = set(reversed(keys))

print
print 's1:', s1
print 's2:', s2
print 's1 == s2:', s1 == s2

A causa della collisione della cache, l'ordine delle chiavi interne dell'elenco è diverso per i due dizionari, anche se essi contengono gli stessi valori e sono considerati uguali. I set usano lo stesso algoritmo di hash, ed offrono lo stesso comportamento.

$ python -m doctest -v doctest_hashed_values.py

d1: {'a': 1, 'aa': 2, 'aaa': 3}
d2: {'aa': 2, 'a': 1, 'aaa': 3}
d1 == d2: True

s1: set(['a', 'aa', 'aaa'])
s2: set(['aa', 'a', 'aaa'])
s1 == s2: True
1 items had no tests:
    doctest_hashed_values
0 tests in 1 items.
0 passed and 0 failed.
Test passed.

Il modo migliore di affrontare queste potenziali discrepanze è creare test che producono valori che cambiano poco probabilmente. Nel caso di set e dizionari, potrebbe significare il cercare chiavi specifiche individualmente, generare un elenco ordinato del contenuto della struttura dati, o il confrontare per uguaglianza con un valore letterale invece di dipendere dalla rappresentazione stringa.

def group_by_length(words):
    """Ritorna un dizionario che raggruppa le parole in insiemi omogenei per lunghezza

    >>> grouped = group_by_length([ 'python', 'modulo', 'della', 'il', 'settimana' ])
    >>> grouped == { 5:set(['della']),
    ...              2:set(['il']),
    ...              9:set(['settimana']),
    ...              6:set(['python', 'modulo']),
    ...              }
    True

    """
    d = {}
    for word in words:
        s = d.setdefault(len(word), set())
        s.add(word)
    return d

Si noti che singolo esempio è in realtà interpretato come due test separati, con il primo che non si attende un output dalla console ed il secondo che si aspetta il risultato booleano dell'operazione di confronto.

$ python -m doctest -v doctest_hashed_values_tests.py
Trying:
    grouped = group_by_length([ 'python', 'modulo', 'della', 'il', 'settimana' ])
Expecting nothing
ok
Trying:
    grouped == { 5:set(['della']),
                 2:set(['il']),
                 9:set(['settimana']),
                 6:set(['python', 'modulo']),
                 }
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.

Traceback

I traceback sono un caso speciali di modifica dati. Visto che i percorsi in un traceback dipendono dalla locazione nella quale un modulo è installato nel filesystem di un dato sistema, sarebbe impossibile scrivere test portabili se essi fossero trattati come un altro output.

def this_raises():
    """Questa funzione solleva sempre una eccezione

    >>> 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("L'errore è qui")
    RuntimeError: L'errore è qui
    """
    raise RuntimeError("L'errore è qui")

doctest compie uno sforzo particolare per riconoscere i traceback, ed ignora le parti che potrebbero cambiare da sistema a sistema.

$ python -m doctest -v doctest_tracebacks.py
Trying:
    this_raises()
Expecting:
    Traceback (most recent call last):
      File "", line 1, in 
      File "/no/such/path/doctest_tracebacks.py", line 14, in this_raises
        raise RuntimeError("L'errore è qui")
    RuntimeError: L'errore è qui
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.

In effetti l'intero corpo del traceback viene ignorato e può essere omesso.

def this_raises():
    """Questa funzione solleva sempre una eccezione

    >>> this_raises()
    Traceback (most recent call last):
    RuntimeError: L'errore è qui
    """
    raise RuntimeError("L'errore è qui")

Quando doctest vede una riga di intestazione di un traceback (sia Traceback (most recent call last): che Traceback (innermost last):, a seconda della versione di Python in esecuzione), salta avanti per trovare il tipo di eccezione ed il messaggio, ignorando le interamente le righe in mezzo.

$ python -m doctest -v doctest_tracebacks_no_body.py
Trying:
    this_raises()
Expecting:
    Traceback (most recent call last):
    RuntimeError: L'errore è qui
ok
1 items had no tests:
    doctest_tracebacks_no_body
1 items passed all tests:
   1 tests in doctest_tracebacks_no_body.this_raises
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Aggirare gli spazi

In una applicazione reale, in genere l'output comprende dei whitespace tipo righe vuote, tabulazioni e spaziatura supplementare usata per favorire la leggibilità. Le righe vuote, in particolare, causano problemi con doctest perchè esse sono usate per delimitare i test.

def double_space(lines):
    """Stampa un elenco di righe con doppia spaziatura

    >>> double_space(['Riga uno.', 'Riga due.'])
    Riga uno.

    Riga due.

    """
    for l in lines:
        print l
        print
    return

double_space() riceve un elenco di righe in input, quindi le stampa a doppia spaziatura intervallate da righe vuote.

$ python -m doctest -v doctest_blankline_fail.py
Trying:
    double_space(['Riga uno.', 'Riga due.'])
Expecting:
    Riga uno.
**********************************************************************
File "doctest_blankline_fail.py", line 7, in doctest_blankline_fail.double_space
Failed example:
    double_space(['Riga uno.', 'Riga due.'])
Expected:
    Riga uno.
Got:
    Riga uno.
    
    Riga due.
    
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.

Il testo fallisce perchè interpreta la riga vuota dopo Riga uno. nella docstring come la fine del campione di output. Per confrontare correttamente le righe vuote occorre sostituirle nell'input di esempio con la stringa <BLANKLINE>.

def double_space(lines):
    """Stampa un elenco di righe con doppia spaziatura

    >>> double_space(['Riga uno.', 'Riga due.'])
    Riga uno.
    <BLANKLINE>
    Riga due.
    <BLANKLINE>
    """
    for l in lines:
        print l
        print
    return

doctest sostituisce le vere righe vuote con l'espressione letterale prima di eseguire il confronto, in modo che valore reale e valore atteso corrispondano ed il test viene superato.

$ python -m doctest -v doctest_blankline.py
Trying:
    double_space(['Riga uno.', 'Riga due.'])
Expecting:
    Riga uno.
    
    Riga due.
    
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.

Un altro pericolo nell'usare comparazioni di testo per le verifiche è che i whitespace incorporati possono anch'essi causare problemi con i test. Questo esempio ha un singolo spazio supplementare dopo il 6.

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

Gli spazi supplementari possono trovare la strada nel proprio codice tramite errori di copia-incolla, ma visto che si trovano alla fine della riga, possono non essere notati nel file sorgente ed essere invisibili anche nel report del fallimento del test.

$ python -m doctest -v doctest_extra_space.py Trying:
    my_function(2, 3)
Expecting:
    6
**********************************************************************
File "doctest_extra_space.py", line 6, in doctest_extra_space.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.

L'uso di una delle opzioni di report basate su diff tipo REPORT_NDIFF, mostra le differenze tra i valori reali e quelli attesi con più dettaglio, e lo spazio extra diventa visibile.

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

Sono disponibili anche le opzioni di diff unificata (REPORT_UDIFF) e contestuale (REPORT_CDIFF), per l'output laddove questi formati siano più leggibili.

$ python -m doctest -v doctest_ndiff.py
Trying:
    my_function(2, 3) #doctest: +REPORT_NDIFF
Expecting:
    6
**********************************************************************
File "doctest_ndiff.py", line 6, in doctest_ndiff.my_function
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.

Ci sono casi nei quali conviene aggiungere whitespace supplementari nell'output campione per il test, e fare sì che doctest li ignori. Ad esempio le strutture dati possono essere più leggibili quando sono stampate in righe diverse, anche se per la loro rappresentazione sarebbe sufficiente una singola riga.

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

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

    Questo non corrisponde perchè c'è uno spazio extra dopo la [ nella lista

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

Quando viene attivata NORMALIZE_WHITESPACE, qualsiasi whitespace nei valori reali ed attesi viene considerato una corrispondenza. Non si possono aggiungere whitespace al valore atteso laddove non ne esistono nell'output, ma la lunghezza della sequenza di whitespace e i reali caratteri whitespace non è necessario che corrisponda. Il primo esempio di test osserva correttamente la regola, e passa, anche se ci sono spazi extra e righe vuote. Il secondo test ha un whitespace supplementare dopo [ e prima di ] e fallisce.

$ python -m doctest -v doctest_normalize_whitespace.py
Trying:
    my_function(['A', 'B', 'C'], 3) #doctest: +NORMALIZE_WHITESPACE
Expecting:
    ['A', 'B', 'C',
     'A', 'B', 'C',
     'A', 'B', 'C']
ok
Trying:
    my_function(['A', 'B', 'C'], 2) #doctest: +NORMALIZE_WHITESPACE
Expecting:
    [ 'A', 'B', 'C',
      'A', 'B', 'C' ]
**********************************************************************
File "doctest_normalize_whitespace.py", line 14, in doctest_normalize_whitespace.my_function
Failed example:
    my_function(['A', 'B', 'C'], 2) #doctest: +NORMALIZE_WHITESPACE
Expected:
    [ 'A', 'B', 'C',
      'A', 'B', 'C' ]
Got:
    ['A', 'B', 'C', 'A', 'B', 'C']
1 items had no tests:
    doctest_normalize_whitespace
**********************************************************************
1 items had failures:
   1 of   2 in doctest_normalize_whitespace.my_function
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

Posizioni dei Test

Fino a qui tutti i test degli esempi sono stati scritti nelle docstring delle funzioni che si stanno verificando. Questo è utile per gli utenti che esaminino le docstring per un aiuto per l'uso della funzione (specialmente con pydoc), ma doctest cerca i test anche in altre posizioni. Una posizione ovvia per test aggiuntivi è nelle altre docstring che si trovano da qualche altra parte nel modulo.

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-


"""I test possono apparire in una qualsiasi docstring all'interno del modulo

I test a livello di modulo oltrepassano i confini di classi e funzioni

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

class A(object):
    """Semplice classe.

    >>> 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(list(self.name)))

class B(A):
    """Un'altra semplice classe

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

Ogni docstring può contenere test a livello di modulo, classe e funzione.

$ python -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.

In casi dove si hanno test che si vogliono includere nel proprio codice sorgente, ma non si vuole che compaiano nell'aiuto per il proprio modulo, occorre metterli in qualche altro posto che non siano le docstring. doctest cerca anche una variabile a livello di modulo chiamata __test__ e la usa per cercare gli altri test. __test__ dovrebbe essere un dizionario che mappa i nomi assegnati ai test (come stringhe) a stringhe, moduli, classi o funzioni.

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):
    """Ritorna a * b
    """
    return a * b

Se il valore associato ad una chiave è una stringa, viene trattata come una docstring ed analizzata per i test. Se il valore è una classe od una funzione, doctest li cerca ricorsivamente per trovare le docstring, le quali sono poi esaminate per trovare i test. In questo esempio, il modulo doctest_private_tests_external ha un singolo test nella docstring.

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):
    """Ritorna a * b
    """
    return a * b

doctest trova un totale di cinque test da eseguire.

$ python -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.

Documentazione Esterna

Mescolare i test nel proprio codice non è l'unico modo per usare doctest. Si possono anche usare esempi incorporati in file di progetti esterni di documentazione, tipo dei file reStructuredText.

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

L'aiuto per doctest_in_help viene salvato in un file separato, doctest_in_help.rst. Gli esempi che illustrano come usare il modulo sono inclusi con il testo di aiuto, e doctest può essere usato per trovarli ed eseguirli.

===============================
 Come Usare doctest_in_help.py
===============================

Questa libreria è molto semplice, visto che usa una sola funzione chiamata
``my_function()``.

Numeri
=======

``my_function()`` ritorna il prodotto dei suoi parametro.  Per i numeri,
quel valore equivale ad usare l'operatore ``*``.

::

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

Funziona anche con valori a virgola mobile.

::

    >>> my_function(2.0, 3)
    6.0

Non-Numeri
===========

Visto che ``*`` è definito anche su tipi di dato diversi dai numeri,
``my_function()`` funziona allo stesso mod se uno dei parametri è una
stringa, lista, o tuple.

::

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

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

I test nel file di testo possono essere eseguiti da riga di comando, proprio come se si trattasse di moduli di sorgenti Python.

$ python -m doctest -v doctest_in_help.rst
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.rst
5 tests in 1 items.
5 passed and 0 failed.
Test passed.

In genere doctest imposta l'ambiente di esecuzione dei test in modo da includere i membri del modulo che sta per essere verificato, quindi i propri test non devono importare il modulo esplicitamente. In questo caso, comunque, i test non sono definiti in un modulo Python, e doctest non conosce come impostare lo spazio dei nomi globale, quindi gli esempi devono eseguire essi stessi il lavoro di importazione. Tutti i test in un certo file condividono lo stesso contesto di esecuzoine, quindi è sufficiente importare una volta sola il modulo all'inizio del file.

Eseguire Test

Gli esempi precedenti usatno tutti l'esecutore di test da riga di comando incorporato in doctest. E' facile e conveniente per un modulo singolo, ma diverrebbe velocemente tedioso se il proprio pacchetto si espande su file multipli. Ci sono diversi approcci alternativi

Per Modulo

Si possono includere istruzioni per eseguire doctest contro la propria sorgente alla fine dei propri moduli. Si usa testmod() senza parametri per verificare il modulo corrente.

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()

Occorre assicurarsi che i test siano eseguiti solamente quando il modulo viene chiamato come programma principale, chiamando testmod() solo se il nome corrente del modulo è __main__.

$ python 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.

Il primo parametro di testmod() è un modulo che contiene del codice che viene esaminato per trovare i test. Questa caratteristica consente di creare script di test separati che importano il proprio codice reale ed eseguono i test in ogni modulo uno dopo l'altro.

import doctest_simple

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

Ora si può costruire una suite di test per il proprio progetto importando ogni modulo ed eseguendone i suoi test.

$ python 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.

Per File

testfile() funziona in modo simile a testmod(), consentendo di chiamare esplicitamente i test in un file esterno dall'interno del proprio programma di test.

import doctest

if __name__ == '__main__':
    doctest.testfile('doctest_in_help.rst')
$ python 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.rst
5 tests in 1 items.
5 passed and 0 failed.
Test passed.

Sia testmod() che testfile() includono parametri opzionali che consentono di controllare il comportamento dei test attraverso le opzioni di doctest, lo spazio globale dei nomi per il test, ecc. Fare riferimento alla documentazione della libreria standard per maggiori dettagli se si necessita di queste caratteristiche -- la maggior parte delle volte non saranno necessarie.

Suite Unittest

Se si usa sia unittest che doctest per verificare lo stesso codice in situazioni diverse, si potrebbe trovare utile l'integrazione di unittest in doctest per eseguire insieme i test. Due classi, DocTestSuite e DocFileSuite creano due suite di test compatibili con l'API di esecuzione dei test di unittest.

import doctest
import unittest

import doctest_simple

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

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

I test da ogni sorgente sono raggruppati in un singolo risultato, invece di essere segnalati individualmente.

$ python doctest_unittest.py
Doctest: doctest_simple.my_function ... ok
Doctest: doctest_in_help.rst ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.078s

OK
robby@robby-desktop:~/pydev/pymotw-it/dumpscripts$ python doctest_unittest.py  -v
Doctest: doctest_simple.my_function ... ok
Doctest: doctest_in_help.rst ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.005s

OK

Contesto dei Test

Il contesto di esecuzione creato da doctest mentre esegue i test contiene una copia dei globali a livello di modulo per il modulo che contiene il proprio codice. Questo isola in qualche modo i test uno dall'altro, in modo che possano con meno probabilità interferire reciprocamente. Ogni sorgente dei test (funzioni, classi, modulo) ha il suo proprio insieme di valori globali.

class TestGlobals(object):

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

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

TestGlobals ha due metodi, one() e two(). I test nella docstring di one() impostano una variabile glovale, ed il test per two() la cerca (non aspettandosi di trovarla).

$ python -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.

Questo non significa che i test non possano interferire gli uni con gli altri, comunque, se essi modificano il contenuto di variabili mutevoli definite nel modulo.

_module_data = {}

class TestGlobals(object):

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

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

La variabile del modulo _module_data viene modificata dai test per one(), facendo fallire il testo per two().

$ python -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 18, in doctest_mutable_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.

Se serve impostare delle variabili globali per i test, per parametrizzarli per un ambiente ad esempio, si possono passare valori a testmod() e testfile() ed impostare il contesto usando dati di cui si ha il controllo.

Vedere anche:

doctest
La documentazione della libreria standard per questo modulo.
The Mighty Dictionary
Presentazione di Brandon Rhodes a PyCon 2010 a proposito delle operazioni interne di dict
difflib
La libreria Python per il calcolo delle differenze, usata per generare l'output di ndiff
Sphinx
Oltre ad essere lo strumento di elaborazione della documentazione della libreria standard Python, Sphinx è stata adottata da molti progetti di terze parti perchè è facile da usare e produce un output pulito in diversi formati digitali e di stampa. Sphinx comprende una estensione per eseguire doctest mentre elabora la propria documentazione, in modo che si possa essere a conoscenza che i propri esempi sono sempre accurati
nose
Esecutore di test di terze parti con supporto per doctest
nose
Esecutore di test di terze parti con supporto per doctest
Manuel
Esecutore di test di terze parti basato sulla documentazione con estrazione di casi di test più avanzata ed integrazione con Sphinx