Python: как запустить unittest.main() для всех исходных файлов в подкаталоге?
Я разрабатываю модуль Python с несколькими исходными файлами, каждый из которых имеет свой собственный тестовый класс, полученный из unittest прямо в источнике. Рассмотрим структуру каталогов:
dirFoo\
test.py
dirBar\
__init__.py
Foo.py
Bar.py
Чтобы проверить Foo.py или Bar.py, я бы добавил это в конце исходных файлов Foo.py и Bar.py:
if __name__ == "__main__":
unittest.main()
И запустите Python в любом из источников, т.е.
$ python Foo.py
...........
----------------------------------------------------------------------
Ran 11 tests in 2.314s
OK
В идеале, я бы "test.py" автоматически выполнил поиск dirBar для любых производных классов unittest и сделал один вызов "unittest.main()". Какой лучший способ сделать это на практике?
Я попытался использовать Python для вызова execfile для каждого *.py файла в dirBar, который запускается один раз для первого .py файла и выйдет из вызывающего test.py, а затем мне придется дублировать мой код, добавив unittest.main() в каждый исходный файл, что нарушает принципы DRY.
Ответы
Ответ 1
Я знал, что есть очевидное решение:
dirFoo\
__init__.py
test.py
dirBar\
__init__.py
Foo.py
Bar.py
Содержимое dirFoo/test.py
from dirBar import *
import unittest
if __name__ == "__main__":
unittest.main()
Запустите тесты:
$ python test.py
...........
----------------------------------------------------------------------
Ran 11 tests in 2.305s
OK
Извините за глупый вопрос.
Ответ 2
По состоянию на Python 2.7 обнаружение тестов автоматизировано в пакете unittest. Из docs:
Unittest поддерживает простое обнаружение тестов. Чтобы быть совместимым с обнаружением теста все тестовые файлы должны быть модулями или пакетами импортируется из каталога верхнего уровня проекта (это означает что их имена должны быть действительными идентификаторами).
Открытие теста выполняется в TestLoader.discover()
, но может также использовать из командной строки. Основное использование командной строки:
cd project_directory
python -m unittest discover
По умолчанию он ищет пакеты с именем test*.py
, но это можно изменить, чтобы вы могли использовать что-то вроде
python -m unittest discover --pattern=*.py
Вместо вашего test.py script.
Ответ 3
Вот мой тестовый код обнаружения, который, похоже, выполняет эту работу. Я хотел удостовериться, что могу легко продлить тесты без необходимости перечислять их в любом из задействованных файлов, но также не записывать все тесты в одном файле Übertest.
Итак, структура
myTests.py
testDir\
__init__.py
testA.py
testB.py
myTest.py выглядит так:
import unittest
if __name__ == '__main__':
testsuite = unittest.TestLoader().discover('.')
unittest.TextTestRunner(verbosity=1).run(testsuite)
Я считаю, что это самое простое решение для написания нескольких тестовых примеров в одном каталоге. Для решения требуется Python 2.7 или Python 3.
Ответ 4
Вам следует попробовать nose. Это библиотека, которая помогает создавать тесты и интегрируется с unittest
или doctest
. Все, что вам нужно сделать, это запустить nosetests
, и он найдет для вас все ваши unittests.
% nosetests # finds all tests in all subdirectories
% nosetests tests/ # find all tests in the tests directory
Ответ 5
Я придумал фрагмент, который может делать то, что вы хотите. Он проходит путь, который вы предоставляете для поиска пакетов/модулей Python, и накапливает набор тестовых наборов из этих модулей, которые затем выполняются сразу.
Самое приятное в том, что он будет работать на всех пакетах, вложенных в указанный вами каталог, и вам не придется вручную изменять импорт при добавлении новых компонентов.
import logging
import os
import unittest
MODULE_EXTENSIONS = set('.py .pyc .pyo'.split())
def unit_test_extractor(tup, path, filenames):
"""Pull ``unittest.TestSuite``s from modules in path
if the path represents a valid Python package. Accumulate
results in `tup[1]`.
"""
package_path, suites = tup
logging.debug('Path: %s', path)
logging.debug('Filenames: %s', filenames)
relpath = os.path.relpath(path, package_path)
relpath_pieces = relpath.split(os.sep)
if relpath_pieces[0] == '.': # Base directory.
relpath_pieces.pop(0) # Otherwise, screws up module name.
elif not any(os.path.exists(os.path.join(path, '__init__' + ext))
for ext in MODULE_EXTENSIONS):
return # Not a package directory and not the base directory, reject.
logging.info('Base: %s', '.'.join(relpath_pieces))
for filename in filenames:
base, ext = os.path.splitext(filename)
if ext not in MODULE_EXTENSIONS: # Not a Python module.
continue
logging.info('Module: %s', base)
module_name = '.'.join(relpath_pieces + [base])
logging.info('Importing from %s', module_name)
module = __import__(module_name)
module_suites = unittest.defaultTestLoader.loadTestsFromModule(module)
logging.info('Got suites: %s', module_suites)
suites += module_suites
def get_test_suites(path):
""":return: Iterable of suites for the packages/modules
present under :param:`path`.
"""
logging.info('Base path: %s', package_path)
suites = []
os.path.walk(package_path, unit_test_extractor, (package_path, suites))
logging.info('Got suites: %s', suites)
return suites
if __name__ == '__main__':
logging.basicConfig(level=logging.WARN)
package_path = os.path.dirname(os.path.abspath(__file__))
suites = get_test_suites(package_path)
for suite in suites:
unittest.TextTestRunner(verbosity=2).run(suite)
Ответ 6
В случае, если это помогает кому-либо, вот подход, к которому я пришел, для решения этой проблемы. У меня был вариант использования, где у меня есть следующая структура каталогов:
mypackage/
tests/
test_category_1/
tests_1a.py
tests_1b.py
...
test_category_2/
tests_2a.py
tests_2b.py
...
...
и я хочу, чтобы все следующие действия работали очевидным образом и чтобы были предоставлены те же аргументы командной строки, которые приняты unittest:
python -m mypackage.tests
python -m mypackage.tests.test_category_1
python -m mypackage.tests.test_category_1.tests_1a
Решением было настроить mypackage/tests/__init__.py
следующим образом:
import unittest
def prepare_load_tests_function (the__path__):
test_suite = unittest.TestLoader().discover(the__path__[0])
def load_tests (_a, _b, _c):
return test_suite
return load_tests
и настроить mypackage/tests/__main__.py
следующим образом:
import unittest
from . import prepare_load_tests_function, __path__
load_tests = prepare_load_tests_function(__path__)
unittest.main()
и скопировать и вставить пустой __init__.py
и следующий __main__.py
в каждый mypackage/tests/test_category_n/
:
import unittest
from .. import prepare_load_tests_function
from . import __path__
load_tests = prepare_load_tests_function(__path__)
unittest.main()
а также добавить стандартный if __name__ == '__main__': unittest.main()
в каждый файл фактических тестов.
(работает для меня на Python 3.3 в Windows, ymmv.)