Рубрики
Без рубрики

doctest – Тестирование с помощью документации

Автор оригинала: Doug Hellmann.

Цель:

Напишите автоматизированные тесты как часть документации для модуля.

doctest тестирует исходный код, выполняя примеры, встроенные в документацию, и проверяя, что они дают ожидаемые результаты. Он работает, анализируя текст справки, чтобы найти примеры, запускать их, а затем сравнивать выходной текст с ожидаемым значением. Многие разработчики считают, что doctest проще в использовании, чем unittest, потому что в его простейшей форме нет API, который нужно изучить перед его использованием. Однако по мере того, как примеры становятся более сложными, отсутствие управления приспособлениями может сделать написание тестов doctest более громоздким, чем использование unittest.

Начиная

Первым шагом к настройке тестов является использование интерактивного интерпретатора для создания примеров, а затем их копирование и вставка в строки документации в модуле. Здесь для my_function () приведены два примера:

doctest_simple.py

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

Для запуска тестов используйте doctest в качестве основной программы с помощью параметра -m . Обычно во время выполнения тестов вывод не производится, поэтому следующий пример включает параметр -v , чтобы сделать вывод более подробным.

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

Примеры обычно не могут служить самостоятельными объяснениями функции, поэтому doctest также позволяет окружающий текст. Он ищет строки, начинающиеся с приглашения интерпретатора ( >>> ), чтобы найти начало тестового примера, и этот случай заканчивается пустой строкой или следующим приглашением интерпретатора. Промежуточный текст игнорируется и может иметь любой формат, если только он не похож на тестовый пример.

doctest_simple_with_docs.py

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

    Works with numbers:

    >>> my_function(2, 3)
    6

    and strings:

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

Окружающий текст в обновленной строке документации делает ее более полезной для читателя. Поскольку он игнорируется doctest , результаты такие же.

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

Обработка непредсказуемого вывода

Есть и другие случаи, когда точный результат может быть непредсказуемым, но все же должен быть протестирован. Например, локальные значения даты и времени и идентификаторы объектов изменяются при каждом запуске теста, точность по умолчанию, используемая в представлении значений с плавающей запятой, зависит от параметров компилятора, а строковые представления контейнерных объектов, таких как словари, могут не быть детерминированными. Хотя эти условия нельзя контролировать, существуют методы борьбы с ними.

Например, в CPython идентификаторы объектов основаны на адресе памяти структуры данных, содержащей объект.

doctest_unpredictable.py

class MyClass:
    pass


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

    >>> unpredictable(MyClass())
    []
    """
    return [obj]

Эти значения id меняются каждый раз, когда программа запускается, потому что она загружается в другую часть памяти.

$ python3 -m doctest -v doctest_unpredictable.py

Trying:
    unpredictable(MyClass())
Expecting:
    []
****************************************************************
File ".../doctest_unpredictable.py", line 17, in doctest_unpredi
ctable.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.

Если тесты включают значения, которые могут измениться непредсказуемым образом, и где фактическое значение не важно для результатов теста, используйте параметр ELLIPSIS , чтобы указать doctest игнорировать части проверочного значения.

doctest_ellipsis.py

class MyClass:
    pass


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

    >>> unpredictable(MyClass()) #doctest: +ELLIPSIS
    []
    """
    return [obj]

Комментарий « #doctest: + ELLIPSIS » после вызова unpredictable () сообщает doctest о необходимости включения ELLIPSIS вариант для теста. ... заменяет адрес памяти в идентификаторе объекта, так что часть ожидаемого значения игнорируется, фактический результат совпадает, и тест проходит.

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

Бывают случаи, когда непредсказуемое значение нельзя игнорировать, потому что это сделает тест неполным или неточным. Например, простые тесты быстро становятся более сложными при работе с типами данных, строковые представления которых несовместимы. Например, строковая форма словаря может изменяться в зависимости от порядка добавления ключей.

doctest_hashed_values.py

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

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

Из-за рандомизации хэша и конфликта ключей порядок внутреннего списка ключей может отличаться для словаря при каждом запуске сценария. Наборы используют один и тот же алгоритм хеширования и демонстрируют одинаковое поведение.

$ 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'}

Лучший способ справиться с этими потенциальными несоответствиями – создать тесты, которые производят значения, которые вряд ли изменятся. В случае словарей и наборов это может означать поиск конкретных ключей по отдельности, создание отсортированного списка содержимого структуры данных или сравнение с буквальным значением на равенство вместо зависимости от строкового представления.

doctest_hashed_values_tests.py

import collections


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

    >>> grouped = group_by_length([ 'python', 'module', 'of',
    ... 'the', 'week' ])
    >>> groupedof']),
    ...              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

Единственный пример фактически интерпретируется как два отдельных теста, первый из которых не ожидает вывода на консоль, а второй ожидает логический результат операции сравнения.

$ python3 -m doctest -v doctest_hashed_values_tests.py

Trying:
    grouped = group_by_length([ 'python', 'module', 'of',
    'the', 'week' ])
Expecting nothing
ok
Trying:
    groupedof']),
                 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.

Tracebacks

Tracebacks – это особый случай изменения данных. Поскольку пути в трассировке зависят от места установки модуля в файловой системе, было бы невозможно писать переносимые тесты, если бы они обрабатывались так же, как и другие выходные данные.

doctest_tracebacks.py

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

    >>> this_raises()
    Traceback (most recent call last):
      File "", line 1, in 
      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 прилагает особые усилия для распознавания обратных трассировок и игнорирования частей, которые могут изменяться от системы к системе.

$ python3 -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('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.

Фактически, все тело трассировки игнорируется и может быть опущено.

doctest_tracebacks_no_body.py

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

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

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

Когда doctest видит строку заголовка трассировки (либо « Traceback (последний вызов последним): », либо « Traceback (самый внутренний последний): », для поддержки различных версий Python), он пропускает вперед, чтобы найти тип исключения и сообщение, полностью игнорируя промежуточные строки.

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

Работа с пробелами

В реальных приложениях вывод обычно включает пробелы, такие как пустые строки, табуляции и дополнительные интервалы, чтобы сделать его более читабельным. В частности, пустые строки вызывают проблемы с doctest , поскольку они используются для разграничения тестов.

doctest_blankline_fail.py

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

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

    Line two.

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

double_space () принимает список входных строк и печатает их с двойным интервалом с пустыми строками между ними.

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

Тест не пройден, поскольку он интерпретирует пустую строку после строки, содержащей Line one. в строке документации, как конец выходного образца. Чтобы сопоставить пустые строки, замените их в образце входных данных строкой .

doctest_blankline.py

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

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

doctest заменяет фактические пустые строки тем же литералом перед выполнением сравнения, так что теперь фактическое и ожидаемое значения совпадают, и тест проходит.

$ python3 -m doctest -v doctest_blankline.py

Trying:
    double_space(['Line one.', 'Line two.'])
Expecting:
    Line one.
    
    Line two.
    
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.

Пробелы в строке также могут вызвать сложные проблемы с тестами. В этом примере после 6 есть один дополнительный пробел.

doctest_extra_space.py

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

Лишние пробелы могут попасть в код из-за ошибок копирования и вставки, но, поскольку они находятся в конце строки, они могут оставаться незамеченными в исходном файле и также быть невидимыми в отчете об ошибках теста.

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

Использование одной из опций отчетов на основе различий, таких как REPORT_NDIFF , показывает разницу между фактическими и ожидаемыми значениями более подробно, а дополнительное пространство становится видимым.

doctest_ndiff.py

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

Также доступны унифицированные ( REPORT_UDIFF ) и контекстные ( REPORT_CDIFF ) различия, для вывода, где эти форматы более читабельны.

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

Бывают случаи, когда полезно добавить лишние пробелы в образец вывода для теста и игнорировать его doctest . Например, структуры данных легче читать, если они распределены по нескольким строкам, даже если их представление поместится в одной строке.

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

Когда NORMALIZE_WHITESPACE включен, любые пробелы в фактическом и ожидаемом значениях считаются совпадающими. Пробелы не могут быть добавлены к ожидаемому значению, если их нет в выводе, но длина последовательности пробелов и фактические символы пробелов не должны совпадать. В первом тестовом примере это правило выполняется правильно, и он проходит, даже если есть лишние пробелы и символы новой строки. У второго есть лишние пробелы после [ и перед ] , поэтому он не работает.

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

Тестовые локации

Все тесты в примерах до сих пор были написаны в строках документации тестируемых функций. Это удобно для пользователей, которые изучают строки документации для получения помощи при использовании функции (особенно с pydoc), но doctest ищет тесты и в других местах. Очевидное место для дополнительных тестов – в строках документации в другом месте модуля.

doctest_docstrings.py

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

Module-level tests cross class and function boundaries.

>>> A('a')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'
    """

Строки документации на уровне модуля, класса и функции могут содержать тесты.

$ python3 -m doctest -v doctest_docstrings.py

Trying:
    A('a')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.

Бывают случаи, когда тесты существуют для модуля, который должен быть включен в исходный код, но не в текст справки для модуля, поэтому их нужно разместить где-нибудь, а не в строках документации. doctest также ищет переменную уровня модуля с именем __test__ и использует ее для поиска других тестов. Значением __test__ должен быть словарь, который сопоставляет имена наборов тестов (как строки) строкам, модулям, классам или функциям.

doctest_private_tests.py

import doctest_private_tests_external

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

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

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

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

    'external': doctest_private_tests_external,
}


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

Если значение, связанное с ключом, является строкой, оно обрабатывается как строка документации и проверяется на наличие тестов. Если значением является класс или функция, doctest рекурсивно ищет в них строки документации, которые затем сканируются на предмет тестов. В этом примере модуль doctest_private_tests_external имеет единственный тест в своей строке документации.

doctest_private_tests_external.py

"""External tests associated with doctest_private_tests.py.

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

После сканирования файла примера doctest находит в общей сложности пять тестов для запуска.

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

Внешняя документация

Смешивание тестов с обычным кодом – не единственный способ использовать doctest . Также можно использовать примеры, встроенные во внешние файлы документации проекта, такие как файлы reStructuredText.

doctest_in_help.py

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

Справка для этого примера модуля сохраняется в отдельном файле doctest_in_help.txt . Примеры, иллюстрирующие, как использовать модуль, включены в текст справки, и doctest можно использовать для их поиска и запуска.

doctest_in_help.txt

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

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

Numbers

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

::

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

It also works with floating-point values.

::

    >>> my_function(2.0, 3)
    6.0

Non-Numbers

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

::

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

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

Тесты в текстовом файле можно запускать из командной строки, как и с исходными модулями 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.

Обычно doctest настраивает среду выполнения теста, чтобы включить в нее элементы тестируемого модуля, поэтому тестам не нужно явно импортировать модуль. Однако в этом случае тесты не определены в модуле Python, и doctest не знает, как настроить глобальное пространство имен, поэтому примеры должны выполнять работу по импорту самостоятельно. Все тесты в данном файле используют один и тот же контекст выполнения, поэтому достаточно импортировать модуль один раз в верхней части файла.

Запуск тестов

Во всех предыдущих примерах используется средство запуска тестов командной строки, встроенное в doctest . Это легко и удобно для одного модуля, но быстро станет утомительным, поскольку пакет распределяется на несколько файлов. Есть несколько альтернативных подходов.

По модулю

Инструкции по запуску doctest для источника могут быть включены в конец модулей.

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

Вызов testmod () только в том случае, если текущее имя модуля – __main__ , гарантирует, что тесты будут запускаться только тогда, когда модуль вызывается как основная программа.

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

Первый аргумент testmod () – это модуль, содержащий код, который нужно сканировать на предмет тестов. Отдельный тестовый скрипт может использовать эту функцию для импорта реального кода и запуска тестов в каждом модуле один за другим.

doctest_testmod_other_module.py

import doctest_simple

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

Набор тестов может быть создан для проекта путем импорта каждого модуля и выполнения его тестов.

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

По файлу

testfile () работает аналогично testmod () , позволяя вызывать тесты явно во внешнем файле из тестовой программы.

doctest_testfile.py

import doctest

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

И testmod () , и testfile () включают дополнительные параметры для управления поведением тестов с помощью параметров doctest . Обратитесь к документации стандартной библиотеки для получения дополнительных сведений об этих функциях – в большинстве случаев они не нужны.

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

Люкс Unittest

Когда и unittest, и doctest используются для тестирования одного и того же кода в разных ситуациях, интеграция unittest в doctest может использоваться для совместного выполнения тестов. Два класса, DocTestSuite и DocFileSuite , создают наборы тестов, совместимые с API средства запуска тестов 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(verbosity2)
runner.run(suite)

Тесты из каждого источника сводятся к одному результату, а не сообщаются отдельно.

$ 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

Тестовый контекст

Контекст выполнения, созданный doctest во время выполнения тестов, содержит копию глобальных переменных уровня модуля для тестового модуля. Каждый источник тестов (функция, класс, модуль) имеет свой собственный набор глобальных значений, чтобы несколько изолировать тесты друг от друга, поэтому они с меньшей вероятностью будут мешать друг другу.

doctest_test_globals.py

class TestGlobals:

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

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

TestGlobals имеет два метода: one () и two () . Тесты в строке документации для one () устанавливают глобальную переменную, а тест для two () ищет ее (ожидая, что не найдет).

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

Это не означает, что тесты не могут взаимодействовать друг с другом, если они изменяют содержимое изменяемых переменных, определенных в модуле.

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

Переменная модуля _module_data изменяется тестами для one () , в результате чего тест для 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 ".../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.

Если для тестов необходимы глобальные значения, например, чтобы параметризовать их для среды, значения можно передать в testmod () и testfile () , чтобы настроить контекст с использованием данных, контролируемых вызывающим абонентом.

Смотрите также

  • стандартная библиотека документации для doctest
  • The Mighty Dictionary – презентация Брэндона Роудса на PyCon 2010 о внутренних операциях dict .
  • diffflib – библиотека вычисления разности последовательностей Python, используемая для вывода ndiff.
  • Sphinx . Помимо того, что он является инструментом обработки документации для стандартной библиотеки Python, Sphinx был принят во многих сторонних проектах, поскольку он прост в использовании и обеспечивает чистый вывод в нескольких цифровых и печатных форматах. Sphinx включает расширение для запуска doctests, как и исходные файлы документации процессов, поэтому примеры всегда точны.
  • py.test – сторонняя программа запуска тестов с поддержкой doctest .
  • нос2 – сторонняя программа запуска тестов с поддержкой doctest .
  • Manuel – средство запуска тестов на основе сторонней документации с более продвинутым извлечением тестовых примеров и интеграцией со Sphinx.