Надежные относительные пути в doctests

У меня есть функция, которая принимает путь к файлу в качестве аргумента.

output = process('path/to/file.txt')

Мне было интересно, смогу ли я легко проверить такую ​​функцию. Я предоставляю пример входного файла где-то в исходном коде и могу сравнить вывод с тем, что я ожидаю (строка, объект python или, возможно, содержимое другого файла).

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

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

Какова обычная установка при подаче стимулов в виде файла?

Я надеюсь услышать некоторые лучшие решения, чем просто «всегда запускать набор тестов из одного и того же рабочего каталога».

РЕДАКТИРОВАНИЕ: я хочу запускать doctests из централизованной точки входа набора тестов.

import doctest
import mymodule

doctest.testmod(mymodule)

person polwel    schedule 17.11.2017    source источник
comment
Какой код настраивает доктесты, на которые здесь ссылаются? Я имею в виду, являются ли они строками документации внутри модуля или набором текстовых файлов?   -  person metatoaster    schedule 17.11.2017
comment
Добавил немного информации. Спасибо за вопрос.   -  person polwel    schedule 17.11.2017


Ответы (1)


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

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

1) (Не рекомендуется, но все равно включен, потому что иногда это работает лучше всего в случаях простейших примеров.) Самый ленивый, но самый стабильный, автономный и кроссплатформенный способ - и он предполагает, что методы открытия файлов выполняются только в тот модуль, который должен был быть протестирован и выполнен с использованием одного и того же вызова (например, open), использование extraglobs можно использовать для замены вызова open. например

from io import StringIO
import doctest
import mymodule

files = {
    'path/to/file1.txt': '... contents of file1.txt ...', 
    'path/to/file2.txt': '... contents of file2.txt ...',
} 

def lazyopen(p, flag='r'):
    result = StringIO(files[p] if flag == 'r' else '')
    result.name = p 
    return result

doctest.testmod(mymodule, extraglobs={'open': lazyopen})

2) Создать настоящий набор тестов, а не использовать встроенные через doctest.testmod

Хотя сокращение полезно, оно слишком ограничено, поскольку является автономным, его нельзя использовать в сочетании с другими наборами тестов, которые могут быть созданы. Рассмотрите возможность создания специального тестового модуля (например, mymodule/tests.py). Обычно я предпочитаю создавать каталог с именем mymodule/tests, с юнит-тестами с именем что-то вроде test_mysubmodule.py и __init__.py, который содержит настройку test_suite, например так

def make_suite():
    import mymodule
    import os

    def setUp(suite):
        suite.olddir = os.getcwd()  # save the current working directory
        os.chdir(targetdir)  # need to define targetdir

    def tearDown(suite):
        os.chdir(suite.olddir)  # restore the original working directory

    return doctest.DocTestSuite(mymodule, setUp=setUp, tearDown=tearDown)

Итак, мы рассмотрели основы, но нужно определить targetdir. Опять же, несколько вещей, которые вы можете рассмотреть:

1) Создайте временный каталог и заполните каталог необходимыми файлами, используя setup и os.chdir к нему, и удалите временный каталог в tearDown. Либо вручную записать данные, хранящиеся в виде строк внутри тестового модуля, либо скопировать из своего проекта, либо извлечь из архива, но как мы их получим? Что приводит к...

2) Если исходные файлы находятся внутри вашего проекта и setuptools доступны/установлены в среде, просто используйте pkg_resources.resource_filename, чтобы получить местоположение, и присвойте ему targetdir. setUp теперь может выглядеть примерно так

    def setUp(suite):
        suite.olddir = os.getcwd()
        targetdir = pkg_resources.resource_filename('mymodule', '')
        os.chdir(targetdir)

Кроме того, наконец, поскольку теперь это настоящий набор тестов, созданный функцией make_suite внутри mymodules.tests, его выполнение должно выполняться с помощью средства запуска тестов, которое, к счастью, включено в стандартную структуру юнит-тестов в виде простой команды, которая может быть сделано так:

$ python -m unittest mymodule.tests.make_suite
.
----------------------------------------------------------------------
Ran 1 test in 0.014s

OK

Кроме того, поскольку это настоящий набор тестов, его можно интегрировать с набором тестов из модуля unittest, чтобы объединить все в один полный набор тестов для всего вашего пакета.

def make_suite():
    # ... the other setup code

    # this loads all unittests in mymodule from `test_*.py` files
    # inside `mymodule.tests`
    test_suite = test_loader.discover(
        'mymodule.tests', pattern='test_*.py')
    test_suite.addTest(
        doctest.DocTestSuite(mymodule, setUp=setUp, tearDown=tearDown))
    return test_suite

Опять же, команда python -m unittest может использоваться для выполнения тестов, возвращенных полным набором тестов.

person metatoaster    schedule 18.11.2017
comment
Большое спасибо! Я очень ценю усилия, которые вы приложили к этому ответу. Кажется, pkg_resources.resource_filename это то, что я искал. Завтра поиграю. Если к тому времени не появится другой ответ, я приму этот. На данный момент есть +1. - person polwel; 19.11.2017
comment
Еще раз спасибо, ваш ответ был очень поучительным и по делу. Для чего бы это ни стоило, теперь я остановился на приспособлении py.test (именно его я использую, а не unittest), которое автоматически применяется ко всем doctests. Он меняет cwd, как вы предложили. Работает хорошо AFAICT. - person polwel; 20.11.2017
comment
Да, py.test в порядке, он значительно упрощает рабочий процесс настройки тестовой системы, (очень) небольшой недостаток заключается в том, что другим, которые хотят протестировать ваш код, потребуется установить py.test в своей среде. - person metatoaster; 20.11.2017