doctest - Test Tramite Documentazione

Scopo: Scrivere test automatici come parte della documentazione per un modulo.

doctest verifica codice sorgente eseguendo esempi incorporati nella documentazione, assicurandosi che essi producano il risultato atteso. Funziona analizzando il testo di aiuto per trovare esempi, eseguirli, quindi confrontandone il risultato con il valore atteso. Molti sviluppatori trovano doctest più facile da usare rispetto ad unittest, visto che, nella sua forma più semplice, non ci sono API da imparare prima di usarlo. Tuttavia, mano a mano che gli esempi diventano più complessi, la mancanza di caratteristiche di gestione dell'impianto può rendere la scrittura di test con doctest meno conveniente rispetto all'uso di unittest.

Iniziare

Il primo passo per impostare doctest è usare l'interprete interattivo per creare esempi, quindi copiarli nelle docstring nel modulo. Qui, my_function() contiene due esempi.

# doctest_simple.py

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 tramite l'opzione -m. In genere non viene prodotto alcun risultato mentre i test sono in esecuzione, quindi il prossimo esempio include l'opzione -v per rendere il risultato più verboso.

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

Gli esempi di chiamata in genere non possono autonomamente servire come spiegazione di una funzione, quindi doctest consente anche di inserire del testo prima o dopo nelle docstring. Cerca righe che iniziano con il prompt dell'interprete (>>>) per trovare l'inizio di un caso di test, che viene chiuso quanto viene trovata una riga vuota o l'inizio del prossimo prompt dell'interprete. Tutto il resto del testo viene ignorato, e può avere qualunque formato fintanto che non possa essere scambiato per un caso di test.

# doctest_simple_with_docs.py

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

    Lavora con numeri:

    >>> my_function(2, 3)
    6

    e stringhe:

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

Il testo circostante nelle docstring aggiornate lo rende più utile al lettore umano. Visto che viene ignorato da doctest il risultato è lo stesso.

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

Gestire Risultati Imprevedibili

Ci sono altri casi dove l'esatto risultato non può essere previsto, ma dovrebbe comunque essere verificabile. Ad esempio data ed ora localizzate ed identificativi di oggetti cambiano ad ogni esecuzione del test, la precisione predefinita usata per la rappresentazione dei valori a virgola mobile dipende dalle opzioni del compilatore, e la rappresentazione stringa di oggetti contenitore tipo dizionari potrebbero non essere deterministica. Sebbene queste condizioni non possano essere controllate, si sono tecniche per gestirle.

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

# doctest_unpredictable.py

class MyClass:
    pass


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

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

Questi valori di identificativo cambiano ogni volta che un programma viene eseguito, visto che viene caricato in parti diverse di memoria.

$ python3 -m doctest -v doctest_unpredictable.py

Trying:
    unpredictable(MyClass())
Expecting:
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
**********************************************************************
File ".../doctest_unpredictable.py", line 9, in doctest_unpredictable.unpredictable
Failed example:
    unpredictable(MyClass())
Expected:
    [<doctest_unpredictable.MyClass object at 0x10055a2d0>]
Got:
    [<doctest_unpredictable.MyClass object at 0x7fe6d87fb630>]
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 i test includono valori che si ritiene cambino in modo imprevedibile, laddove il valore effettivo non sia importante per il risultato del test, si usi l'opzione ELLIPSIS per dire a doctest di ignorare porzioni di verifica del valore.

# doctest_ellipsis.py

class MyClass:
    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 '#doctest: +ELLIPSIS' dopo la chiamata ad unpredictable() dice a doctest di attivare l'opzione ELLIPSIS per quel test. La stringa '...' rimpiazza l'indirizzo di memoria nell'identificativo dell'oggetto, quindi quella porzione del valore atteso viene ignorata e il risultato effettivo corrisponde ed il test viene superato.

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

Ci sono casi nei quali il valore non prevedibile non può essere ignorato, poichè renderebbe il test incompleto od inaccurato. Ad esempio dei semplici test diventano velocemente più complicati quando si ha a che fare con tipi di dati la cui rappresentazione stringa è inconsistente. Il formato stringa di un dizionario, ad esempio, potrebbe cambiare in base all'ordine nel quale vengono aggiunte le chiavi.

# doctest_hashed_values.py

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

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

Visto la casualità della procedura di generazione casuale di hashing e delle potenziali collisioni di chiavi, la lista interna delle chiavi potrebbe essere in ordine diverso nel dizionario ogni volta che viene eseguito lo script.

$ python3 doctest_hashed_values.py

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

Gli insiemi (set) usano lo stesso algoritmo di hash e mostrano lo stesso problema.

$ python3 doctest_hashed_values.py

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

Il modo migliore per gestire queste potenziali discrepanze è creare test che producono valori che difficilmente possono cambiare. Nel caso di dizionari ed insiemi, potrebbe voler dire cercare individualmente specifiche chiavi, generando una lista ordinata dei contenuti della struttura dati, oppure eseguire un confronto per uguaglianza contro un valore letterale invece che dipendere dalla rappresentazione stringa.

# doctest_hashed_values_tests.py

import collections


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

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

Il singolo esempio è in realtà interpretato come due test separati, con il promo che non si attende un risultato da console ed il secondo che si attende il booleano risultante dall'operazione di confronto.

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

Traceback

I traceback rappresentano un caso speciale di dati in cambiamento . Visto che i percorsi in un traceback dipendono dalla locazione nella quale è installato un modulo nel file system, sarebbe impossibile scrivere test portabili se fossero trattati alla stregua di altro input.

# doctest_tracebacks.py

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 ignorare le parti che potrebbero cambiare da sistema a sistema.

$ 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("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 è ignorato e può essere omesso.

# doctest_tracebacks_no_body.py

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

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

    >>> this_raises()
    Traceback (innermost 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):", per supportare diverse versioni di Python), salta in avanti fino a trovare il tipo di eccezione ed il messaggio ignorando interamente le righe intermedie.

$ python3 -m doctest -v doctest_tracebacks_no_body.py

Trying:
    this_raises()
Expecting:
    Traceback (most recent call last):
    RuntimeError: L'errore è qui
ok
Trying:
    this_raises()
Expecting:
    Traceback (innermost last):
    RuntimeError: L'errore è qui
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.

Gestire Spazi, Righe Vuote, Tabulazioni

Nelle vere applicazioni, il risultato in genere include righe vuote, spazi, tabulazioni ed altre spaziature per renderlo meglio leggibile. Le righe vuote, in particolare, causano problemi con doctest in quanto sono usate per delimitare i test.

# doctest_blankline_fail.py

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

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

    Riga due.

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

double_space() richiede una lista di righe in input, e le stampa con doppia spaziatura di riga per ognuna.

$ python3 -m doctest -v doctest_blankline_fail.py

Trying:
    double_space(['Riga uno.', 'Riga due.'])
Expecting:
    Riga uno.
**********************************************************************
File "/dati/dev/python/pymotw3restyling/dumpscripts/doctest_blankline_fail.py", line 6, in doctest_blankline_fail.double_space
Failed example:
    double_space(['Riga uno.', 'Riga due.'])
Expected:
    Riga uno.
Got:
    Riga uno.
    <BLANKLINE>
    Riga due.
    <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.

Il test fallisce visto che interpreta la riga vuota come la riga che contiene Riga uno. nella docstring alla fine del risultato di esempio. Per far corrispondere le righe vuote, si rimpiazzino nell'input di esempio con la stringa <BLANKLINE>.

# doctest_blankline.py

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

doctest sostituisce le vere righe vuote con lo stesso letterale prima di eseguire il confronto, in questo modo i valori reali e quelli attesi corrispondono e il test viene superato.

$ python3 -m doctest -v doctest_blankline.py

Trying:
    double_space(['Riga uno.', 'Riga due.'])
Expecting:
    Riga uno.
    <BLANKLINE>
    Riga due.
    <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.

Anche caratteri di spaziatura all'interno di una riga possono causare problemi con i test. Questo esempio ha uno spazio extra dopo il 6.

# doctest_extra_space.py

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

Gli spazi extra possono essere inseriti nel codice tramite errori di copia e incolla, visto che però vengono inseriti alla fine della riga potrebbero passare inosservati nel file sorgente ed essere parimenti invisibili nel rapporto del fallimento del test.

$ python3 -m doctest -v doctest_extra_space.py

Trying:
    my_function(2, 3)
Expecting:
    6 
**********************************************************************
File "/dati/dev/python/pymotw3restyling/dumpscripts/doctest_extra_space.py", line 5, 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.

Utilizzando una delle opzioni di rapporto basate su diff come REPORT_NDIFF, mostra la differenza tra i valori attesi e reali con maggior dettaglio e gli spazi extra diventano visibili.

# doctest_ndiff.py

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

Sono disponibili diff unificati (REPORT_UDIFF) e di contesto (REPORT_CDIFF), per risultati dove questi formati risultano maggiormente leggibili.

$ python3 -m doctest -v doctest_ndiff.py

Trying:
    my_function(2, 3) #doctest: +REPORT_NDIFF
Expecting:
    6 
**********************************************************************
File "/dati/dev/python/pymotw3restyling/dumpscripts/doctest_ndiff.py", line 5, 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 è conveniente aggiungere caratteri di spaziatura extra nel risultato di esempio per il test, facendo in modo che doctest li ignori. Ad esempio, le strutture dati possono essere più facili da leggere quando vengono piazzate su diverse righe, anche se per la loro rappresentazione basterebbe una singola riga.

# doctest_normalize_whitespace.py

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

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

    Questo non corrisponde a causa di spazi extra dopo la [ nella
    lista.

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

Con NORMALIZE_WHITESPACE attivato, qualsiasi carattere di spaziatura nei valori reali ed attesi viene considerato una corrispondenza. I caratteri di spaziatura non possono essere aggiunti ai valori attesi dove non esistono nel risultato, ma la lunghezza della sequenza di caratteri di spaziatura ed i reali caratteri di spaziatura non devono necessariamente corrispondere. Il primo esempio di test interpreta correttamente questa regola, e viene superato, anche se ci sono caratteri di spaziatura extra e ritorni a capo. Il secondo ha caratteri di spaziatura extra dopo la [ e prima di ], quindi fallisce.

$ python3 -m doctest -v doctest_normalize_whitespace.py

Trying:
    my_function(['A', 'B'], 3)  #doctest: +NORMALIZE_WHITESPACE
Expecting:
    ['A', 'B',
     'A', 'B',
     'A', 'B']
ok
Trying:
    my_function(['A', 'B'], 2) #doctest: +NORMALIZE_WHITESPACE
Expecting:
    [ 'A', 'B',
      'A', 'B', ]
**********************************************************************
File "/dati/dev/python/pymotw3restyling/dumpscripts/doctest_normalize_whitespace.py", line 14, in doctest_normalize_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:
   1 of   2 in doctest_normalize_whitespace.my_function
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

Locazione dei test

Finora tutti i test di esempio sono stati scritti nelle docstring delle funzioni nelle quali sono verificati. Questo è conveniente per gli utenti che esaminano le docstring per un aiuto sull'uso della funzione (specialmente con pydoc), ma doctest cerca anche in altri posti. La locazione ovvia per test aggiuntivi è nelle docstring altrove nel modulo.

# doctest_docstrings.py

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


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

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

Tutte le docstring a livello di modulo, classe e funzione possono contenere test.

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

Ci sono casi nei quali esistono test per un modulo che dovrebbero essere inclusi nel codice sorgente ma non nel testo di aiuto per il modulo, quindi non devono essere piazzati nelle docstring ma da qualche altra parte. doctest cerca anche una variabile a livello di modulo chiamata __test__ e la usa per localizzare altri test. Il valore di __test__ deve essere un dizionario che mappa i nomi dell'insieme di test (come stringa) alle stringhe, moduli, classi o funzioni.

# doctest_private_tests.py

import doctest_private_tests_external

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

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

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

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

    'esterni': doctest_private_tests_external,
}


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

Se il valore associato alla chiave è una stringa, viene trattato come una docstring ed esaminata per i test. Se il valore è una classe o funzione doctest ne cerca ricorsivamente la docstring, che poi sono esaminate per i test. In questo esempio, il modulo doctest_private_tests_external ha un singolo test nella sua docstring.

# doctest_private_tests_external.py

"""Test esterni associali a doctest_private_tests.py.

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

Dopo aver esaminato il file di esempio, doctest trova un totale di cinque test da eseguire.

$ 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__.esterni
   2 tests in doctest_private_tests.__test__.nummeri
   2 tests in doctest_private_tests.__test__.stringhe
5 tests in 5 items.
5 passed and 0 failed.
Test passed.

Documentazione Esterna

Combinare test e normale codice non è il solo modo per utilizzare doctest. Si possono usare anche test incorporati in file esterni di documentazione di progetto, tipo i file in formato reStructuredText.

# doctest_in_help.py

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

L'aiuto per questo modulo di esempio viene salvato in un file separato: doctest_in_help.txt. Gli esempi che illustrano l'uso del modulo sono inclusi nel testo di aiuto, e doctest può essere usato per trovarli ed eseguirli.

# doctest_in_help.txt

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

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 ``*`` si definisce anche su tipi di dato diversi dai numeri,
``my_function()`` funziona allo stesso mod se uno dei parametri
rappresenta un stringa, lista, o tupla.

::

    >>> 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 per qualsiasi altro modulo sorgente 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 imposta l'ambiente di esecuzione dei test in modo da includere i membri del modulo da verificare, quindi i test non devono importare esplicitamente il modulo. In questo caso, comunque, i test non sono definiti in un modulo Python, e doctest non sa come impostare lo spazio dei nomi globale, quindi gli esempi devono eseguire essi stessi l'importazione. Tutti i test in un dato file condividono lo stesso contesto di esecuzione, quindi è sufficiente importare il modulo una volta all'inizio del file.

Eseguire i Test

Gli esempi precedenti usano tutti l'esecutore da riga di comando inserito all'intero di doctest. E' facile e conveniente per un singolo modulo, ma diventerebbe rapidamente tedioso quando il pacchetto è sparso su diversi file. Ci sono diversi approcci alternativi.

Per Modulo

Le istruzioni per eseguire doctest contro il sorgente possono essere incluse alla fine dei moduli.

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

Se si chiama testmod() solo se il nome del modulo corrente è __main__ assicura che i test siano eseguiti solamente quando il modulo viene invocato come programma principale.

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

Il primo argomento di testmod() è un modulo che contiene codice da esaminare per trovare i test. Uno script di test separato può usare questa caratteristica per importare il vero codice ed eseguire i test in ciascun modulo, uno dopo l'altro.

# doctest_testmod_other_module.py

import doctest_simple

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

Un insieme di test può essere costruito per il progetto importando ciascun modulo ed eseguendo i propri test.

$ 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.
Da File

testfile() funziona in modo simile a testmod(). consentendo l'invocazione dei test esplicitamente in un file esterno dall'interno del programma.

# doctest_testfile.py

import doctest

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

Sia testmod() che testfile() includono parametri opzionali per controllare il comportamento dei test tramite le opzioni di doctest. Si faccia riferimento alla documentazione della libreria standard per maggiori dettagli su queste caratteristiche -- la maggior parte delle volte non sono necessarie.

$ 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.
Suite Unittest

Quando sia unittest che doctest sono utilizzati per verificare lo stesso codice in diverse situazioni, l'integrazione di unittest in doctest può essere utilizzata per eseguire i test insieme. Due classi, DocTestSuite e DocFileSuite creano insiemi di test compatibili con l'API per l'esecuzione dei test di 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)

I test da ciascuna sorgente sono unificati in un unico risultato, invece che essere riportati 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

Contesto di Test

Il contesto di esecuzione creato da doctest mentre esegue i test contiene una copia dei globali a livello di modulo per il modulo di test. Ciascun sorgente dei test (funzione, classe, modulo) ha il proprio insieme di valori globali per isolare in qualche modo i test gli uni dagli altri, in modo che sia meno probabile che interferiscano tra loro.

# doctest_test_globals.py

class TestGlobals:

    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 per one() impostano una variabile globale, ed i test per two() la cercano (attendendosi di non trovarla).

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

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

# 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 variabile di modulo _module_data viene modificata dai test per one(), facendo fallire il test per two().

$ 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 "/dati/dev/python/pymotw3restyling/dumpscripts/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 valori globali sono necessari per i test, ad esempio per parametrizzarli per un ambiente, i valori possono essere passati a testmod() e testfile() per impostare il contesto utilizzando dati controllati dall'utente.

Vedere anche:

doctest
La documentazione della libreria standard per questo modulo.
The Mighty Dictionary
Presentazione di Brandon Rhodes a PyCon 2010 circa le operazioni interne dei dizionari.
difflib
Libreria per il calcolo delle differenze, usata per produrre il risultato in formato ndiff.
Sphinx
Oltre ad essere uno strumento di elaborazione della documentazione per la libreria standard di Python, Sphinx è stato adottato da molti progetti di terze parti visto che è facile da usare e produce un output pulito in parecchi formati digitali e di stampa. Sphinx include una estensione per eseguire i doctest mentre elabora i file sorgente della documentazione, in modo che gli esempi siano sempre accurati.
py.test
Esecutore di test di terze parti con supporto per doctest.
nose2
Esecutore di test di terze parti con supporto per doctest.
Manuel
Esecutore di test basato sulla documentazione di terze parti con estrazione di casi di test più avanazati ed integrazione con Sphinx.