Как запустить все модульные тесты Python в каталоге?

315

У меня есть каталог, содержащий мои модульные тесты Python. Каждый модуль модульного тестирования имеет форму теста _ *. Py . Я пытаюсь создать файл с именем all_test.py , который, как вы уже догадались, запустит все файлы в вышеупомянутой тестовой форме и вернет результат. Я пробовал два метода до сих пор; оба потерпели неудачу. Я покажу два метода, и я надеюсь, что кто-то там знает, как на самом деле сделать это правильно.

Для моей первой отважной попытки я подумал: «Если я просто импортирую все свои модули тестирования в файл, а затем назову этот unittest.main()doodad, он будет работать, верно?» Ну, оказывается, я был не прав.

import glob
import unittest

testSuite = unittest.TestSuite()
test_file_strings = glob.glob('test_*.py')
module_strings = [str[0:len(str)-3] for str in test_file_strings]

if __name__ == "__main__":
     unittest.main()

Это не сработало, результат, который я получил:

$ python all_test.py 

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Для моей второй попытки, хотя, хорошо, может быть, я попытаюсь сделать все это тестирование более «ручным» способом. Поэтому я попытался сделать это ниже:

import glob
import unittest

testSuite = unittest.TestSuite()
test_file_strings = glob.glob('test_*.py')
module_strings = [str[0:len(str)-3] for str in test_file_strings]
[__import__(str) for str in module_strings]
suites = [unittest.TestLoader().loadTestsFromName(str) for str in module_strings]
[testSuite.addTest(suite) for suite in suites]
print testSuite 

result = unittest.TestResult()
testSuite.run(result)
print result

#Ok, at this point I have a result
#How do I display it as the normal unit test command line output?
if __name__ == "__main__":
    unittest.main()

Это тоже не сработало, но похоже так близко!

$ python all_test.py 
<unittest.TestSuite tests=[<unittest.TestSuite tests=[<unittest.TestSuite tests=[<test_main.TestMain testMethod=test_respondes_to_get>]>]>]>
<unittest.TestResult run=1 errors=0 failures=0>

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Кажется, у меня есть какой-то набор, и я могу выполнить результат. Я немного обеспокоен тем фактом, что в нем говорится только run=1, кажется, что так и должно быть run=2, но это прогресс. Но как мне передать и отобразить результат на главном? Или как мне заставить его работать, чтобы я мог просто запустить этот файл, и при этом запустить все модульные тесты в этом каталоге?

Стивен Кэгл
источник
1
Перейдите к ответу Трэвиса, если вы используете Python 2.7+
Rocky
Вы когда-нибудь пробовали запустить тесты из объекта тестового экземпляра?
Буратино
Посмотрите этот ответ для решения с примером структуры файла.
Дерек Соике

Ответы:

477

С Python 2.7 и выше вам не нужно писать новый код или использовать сторонние инструменты для этого; Выполнение рекурсивного теста через командную строку является встроенным. Поместите __init__.pyв свой тестовый каталог и:

python -m unittest discover <test_directory>
# or
python -m unittest discover -s <directory> -p '*_test.py'

Вы можете прочитать больше в документации по тестированию модулей Python 2.7 или Python 3.x.

Трэвис Медведь
источник
11
проблемы включают: ImportError: начальный каталог не импортируется:
zinking
6
По крайней мере, в Python 2.7.8 в Linux ни один вызов командной строки не дает мне рекурсии. В моем проекте есть несколько подпроектов, чьи модульные тесты находятся в соответствующих каталогах "unit_tests / <subproject> / python /". Если я укажу такой путь, то будут запущены модульные тесты для этого подпроекта, но при использовании только «unit_tests» в качестве аргумента каталога тестов тесты не будут найдены (вместо всех тестов для всех подпроектов, как я надеялся). Любой намек?
user686249 15.07.15
6
О рекурсии: первая команда без <test_directory> по умолчанию имеет значение "." и возвращается к подмодулям . То есть все каталоги тестов, которые вы хотите обнаружить, должны иметь init .py. Если они это сделают, они будут найдены командой Discover. Только что попробовал, это сработало.
Эмиль Стенстрём
Это сработало для меня. У меня есть папка тестов с четырьмя файлами, запустите ее с моего терминала Linux, отличные вещи.
JasTonAChair 22.09.16
5
Спасибо! Почему это не принятый ответ? На мой взгляд, лучшим ответом всегда будет тот, который не требует каких-либо внешних зависимостей ...
Джонатан Бенн
108

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

Обновлено:

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

testmodules = [
    'cogapp.test_makefiles',
    'cogapp.test_whiteutils',
    'cogapp.test_cogapp',
    ]

suite = unittest.TestSuite()

for t in testmodules:
    try:
        # If the module defines a suite() function, call it to get the suite.
        mod = __import__(t, globals(), locals(), ['suite'])
        suitefn = getattr(mod, 'suite')
        suite.addTest(suitefn())
    except (ImportError, AttributeError):
        # else, just load all the test cases from the module.
        suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t))

unittest.TextTestRunner().run(suite)
Нед Бэтчелдер
источник
2
Преимущество этого подхода перед простым явным импортом всех ваших тестовых модулей в один модуль test_all.py и вызовом unittest.main () в том, что вы можете опционально объявить набор тестов в одних модулях, а не в других?
Кори Портер
1
Я опробовал нос, и он отлично работает. Это было легко установить и запустить в моем проекте. Я даже смог автоматизировать это с помощью нескольких строк сценария, работающих внутри virtualenv. +1 за нос!
Джесси Уэбб
Это не всегда выполнимо: иногда импорт структуры проекта может привести к путанице, если он попытается запустить импорт модулей.
Чиффа
4
Обратите внимание, что в течение последних нескольких лет программа « Нос » находилась в «режиме обслуживания», и в настоящее время рекомендуется использовать « Нос2» , « pytest» или просто обычный unittest / unittest2 для новых проектов.
Курт Пик
Вы когда-нибудь пробовали запустить тесты из объекта тестового экземпляра?
Буратино
96

В Python 3, если вы используете unittest.TestCase:

  • У вас должен быть пустой (или другой) __init__.pyфайл в вашем testкаталоге ( должен быть назван test/)
  • Ваши тестовые файлы внутри test/соответствуют шаблону test_*.py. Они могут находиться внутри подкаталога test/, и эти подкаталоги могут быть названы как угодно.

Затем вы можете запустить все тесты с помощью:

python -m unittest

Готово! Решение менее 100 строк. Надеюсь, другой начинающий питон сэкономит время, найдя это.

tmck-код
источник
3
Обратите внимание, что по умолчанию он ищет только тесты в именах файлов, начинающихся с «test»
Shawabawa
3
Правильно, первоначальный вопрос касался того факта, что «Каждый модуль модульного тестирования имеет форму test _ *. Py.», Поэтому этот ответ в прямом ответе. Теперь я обновил ответ, чтобы он был более явным
tmck-code
1
Спасибо, что мне не хватило, чтобы использовать ответ Трэвиса Медведя.
Джереми Кочой
65

Теперь это возможно прямо из unittest: unittest.TestLoader.discover .

import unittest
loader = unittest.TestLoader()
start_dir = 'path/to/your/test/files'
suite = loader.discover(start_dir)

runner = unittest.TextTestRunner()
runner.run(suite)
slaughter98
источник
3
Я попробовал этот метод также, есть пара тестов, но работает отлично. Превосходно!!! Но мне любопытно, у меня только 4 теста. Вместе они запускают 0,032 с, но когда я использую этот метод для их запуска, я получаю результат .... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OKПочему? Разница, откуда она берется?
Симкус,
У меня проблемы с запуском файла, который выглядит так из командной строки. Как это должно быть вызвано?
Дастин Майклс
python file.py
бойня98
1
Работал без нареканий! Просто установите его в свой test / dir и затем установите start_id = "./". ИМХО, этот ответ сейчас (Python 3.7) принят!
jjwdesign
Вы можете изменить последнюю строку на ´res = runner.run (suite); sys.exit (0, если res.wasSuccessful () else 1) ´, если вам нужен правильный код выхода
Sadap
32

Ну, немного изучив приведенный выше код (в частности, используя TextTestRunnerи defaultTestLoader), я смог довольно близко подойти. В конце концов я исправил свой код, просто передав все наборы тестов одному конструктору наборов, а не добавляя их «вручную», что решило другие мои проблемы. Так вот мое решение.

import glob
import unittest

test_files = glob.glob('test_*.py')
module_strings = [test_file[0:len(test_file)-3] for test_file in test_files]
suites = [unittest.defaultTestLoader.loadTestsFromName(test_file) for test_file in module_strings]
test_suite = unittest.TestSuite(suites)
test_runner = unittest.TextTestRunner().run(test_suite)

Да, возможно, проще просто использовать нос, чем делать это, но это не главное.

Стивен Кэгл
источник
хорошо, он отлично работает для текущего каталога, как вызвать суб-напрямую?
Ларри Кай
Ларри, см. Новый ответ ( stackoverflow.com/a/24562019/104143 ) для обнаружения рекурсивных тестов
Питер Кофлер
Вы когда-нибудь пробовали запустить тесты из объекта тестового экземпляра?
Буратино
25

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

from unittest import TestLoader, TextTestRunner, TestSuite
from uclid.test.test_symbols import TestSymbols
from uclid.test.test_patterns import TestPatterns

if __name__ == "__main__":

    loader = TestLoader()
    tests = [
        loader.loadTestsFromTestCase(test)
        for test in (TestSymbols, TestPatterns)
    ]
    suite = TestSuite(tests)

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

где uclidмой проект и TestSymbolsи TestPatternsявляются подклассами TestCase.

сумасшедший ежик
источник
Из документа unittest.TestLoader : «Обычно нет необходимости создавать экземпляр этого класса; модуль unittest предоставляет экземпляр, который можно использовать как unittest.defaultTestLoader». Кроме того, поскольку в качестве аргумента TestSuiteпринимается итерируемое , вы можете создать итеративное в цикле, чтобы избежать повторения loader.loadTestsFromTestCase.
Двухразрядный алхимик
@ Двухразрядный алхимик, твое второе замечание особенно приятно. Я бы изменил код, чтобы включить, но я не могу проверить его. (Первый мод сделал бы его похожим на Java по моему вкусу ... хотя я понимаю, что я иррациональна (привяжите их к именам переменных верблюжьего кейса)).
сумасшедший ежик
Это мой фаворит, очень чистый. Был в состоянии упаковать это и сделать это аргументом в моей обычной командной строке.
MarkII
15

Я использовал discoverметод и перегрузку load_testsдля достижения этого результата в (минимальных, я думаю) числовых строках кода:

def load_tests(loader, tests, pattern):
''' Discover and load all unit tests in all files named ``*_test.py`` in ``./src/``
'''
    suite = TestSuite()
    for all_test_suite in unittest.defaultTestLoader.discover('src', pattern='*_tests.py'):
        for test_suite in all_test_suite:
            suite.addTests(test_suite)
    return suite

if __name__ == '__main__':
    unittest.main()

Исполнение на пятерках что-то вроде

Ran 27 tests in 0.187s
OK
выстр
источник
это доступно только для python2.7, я думаю
Ларри Кай
@larrycai Может быть, я обычно на Python 3, иногда Python 2.7. Вопрос не был привязан к конкретной версии.
Rds
Я на Python 3.4 и обнаруживает, возвращает набор, делая цикл лишним.
Дюны
Для будущего Ларри: «Многие новые функции были добавлены в unittest в Python 2.7, включая обнаружение тестов. Unittest2 позволяет использовать эти функции с более ранними версиями Python».
Двухразрядный алхимик
8

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

find . -name 'Test*py' -exec python '{}' \;

и самое главное, это определенно работает.

Кованые изделия
источник
7

В случае упакованной библиотеки или приложения вы не хотите этого делать. setuptools сделаю это для вас .

Чтобы использовать эту команду, тесты вашего проекта должны быть помещены в набор unittestтестов с помощью функции, класса или метода TestCase или модуля или пакета, содержащего TestCaseклассы. Если названный набор является модулем, и у модуля есть additional_tests()функция, он вызывается, и результат (который должен быть a unittest.TestSuite) добавляется к выполняемым тестам. Если названный набор является пакетом, любые субмодули и подпакеты рекурсивно добавляются в общий набор тестов .

Просто скажите, где находится ваш корневой тестовый пакет, например:

setup(
    # ...
    test_suite = 'somepkg.test'
)

И беги python setup.py test.

Обнаружение на основе файлов может быть проблематичным в Python 3, если только вы не избегаете относительного импорта в свой набор тестов, потому что discoverиспользует импорт файлов. Несмотря на то, что он поддерживает необязательные top_level_dir, но у меня были бесконечные ошибки рекурсии. Таким образом, простое решение для неупакованного кода - поместить в __init__.pyваш тестовый пакет следующее (см. Протокол load_tests ).

import unittest

from . import foo, bar


def load_tests(loader, tests, pattern):
    suite = unittest.TestSuite()
    suite.addTests(loader.loadTestsFromModule(foo))
    suite.addTests(loader.loadTestsFromModule(bar))

    return suite
SAAJ
источник
Хороший ответ, и его можно использовать для автоматизации тестирования перед развертыванием! Спасибо
Артур Клерк-Герарди
4

Я использую PyDev / LiClipse и не совсем понял, как запустить все тесты сразу из графического интерфейса. (редактировать: вы щелкаете правой кнопкой мыши корневую тестовую папку и выбираетеRun as -> Python unit-test

Это мой текущий обходной путь:

import unittest

def load_tests(loader, tests, pattern):
    return loader.discover('.')

if __name__ == '__main__':
    unittest.main()

Я поместил этот код в модуль, который называется allв моей тестовой директории. Если я запускаю этот модуль как юнит-тест из LiClipse, тогда все тесты запускаются. Если я попрошу повторить только определенные или неудачные тесты, то будут запущены только эти тесты. Это также не мешает моему тестировщику командной строки (тесты на нос) - оно игнорируется.

Возможно, вам придется изменить аргументы на discoverоснове настроек вашего проекта.

Дюны
источник
Имена всех тестовых файлов и методов тестирования должны начинаться с «test_». В противном случае команда «Выполнить как -> тест модуля Python» не найдет их.
Стефан,
2

Основываясь на ответе Стивена Кейгла, я добавил поддержку вложенных тестовых модулей.

import fnmatch
import os
import unittest

def all_test_modules(root_dir, pattern):
    test_file_names = all_files_in(root_dir, pattern)
    return [path_to_module(str) for str in test_file_names]

def all_files_in(root_dir, pattern):
    matches = []

    for root, dirnames, filenames in os.walk(root_dir):
        for filename in fnmatch.filter(filenames, pattern):
            matches.append(os.path.join(root, filename))

    return matches

def path_to_module(py_file):
    return strip_leading_dots( \
        replace_slash_by_dot(  \
            strip_extension(py_file)))

def strip_extension(py_file):
    return py_file[0:len(py_file) - len('.py')]

def replace_slash_by_dot(str):
    return str.replace('\\', '.').replace('/', '.')

def strip_leading_dots(str):
    while str.startswith('.'):
       str = str[1:len(str)]
    return str

module_names = all_test_modules('.', '*Tests.py')
suites = [unittest.defaultTestLoader.loadTestsFromName(mname) for mname 
    in module_names]

testSuite = unittest.TestSuite(suites)
runner = unittest.TextTestRunner(verbosity=1)
runner.run(testSuite)

Код ищет все подкаталоги .для *Tests.pyфайлов , которые затем загружаются. Ожидается, что каждый из них *Tests.pyбудет содержать один класс, *Tests(unittest.TestCase)который загружается по очереди и выполняется один за другим.

Это работает с произвольным глубоким вложением каталогов / модулей, но каждый каталог между ними должен содержать как __init__.pyминимум пустой файл. Это позволяет тесту загружать вложенные модули, заменяя косую черту (или обратную косую черту) точками (см. replace_slash_by_dot).

Питер Кофлер
источник
2

Это старый вопрос, но что сработало для меня сейчас (в 2019 году):

python -m unittest *_test.py

Все мои тестовые файлы находятся в той же папке, что и исходные файлы, и они заканчиваются на _test.

Пластическая роща
источник
1

Этот BASH-скрипт будет выполнять тестовый каталог python unittest из ЛЮБОГО места в файловой системе, независимо от того, в каком рабочем каталоге вы находитесь: его рабочий каталог всегда будет находиться там, где находится этот testкаталог.

ВСЕ ИСПЫТАНИЯ, независимые $ PWD

Модуль Python unittest чувствителен к вашему текущему каталогу, если вы не укажете его где (используя discover -sопцию).

Это полезно, когда вы остаетесь в рабочем ./srcили ./exampleрабочем каталоге, и вам нужен быстрый общий юнит-тест:

#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

python -m unittest discover -s "$readlink"/test -v

ИЗБРАННЫЕ ИСПЫТАНИЯ, независимые $ PWD

Я называю этот служебный файл: runone.pyи использую его так:

runone.py <test-python-filename-minus-dot-py-fileextension>
#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

(cd "$dirname"/test; python -m unittest $1)

Не нужно, чтобы test/__init__.pyфайл загружал ваши пакеты / память во время производства.

Джон Грин
источник
-3

Вот мой подход к созданию оболочки для запуска тестов из командной строки:

#!/usr/bin/env python3
import os, sys, unittest, argparse, inspect, logging

if __name__ == '__main__':
    # Parse arguments.
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-?", "--help",     action="help",                        help="show this help message and exit" )
    parser.add_argument("-v", "--verbose",  action="store_true", dest="verbose",  help="increase output verbosity" )
    parser.add_argument("-d", "--debug",    action="store_true", dest="debug",    help="show debug messages" )
    parser.add_argument("-h", "--host",     action="store",      dest="host",     help="Destination host" )
    parser.add_argument("-b", "--browser",  action="store",      dest="browser",  help="Browser driver.", choices=["Firefox", "Chrome", "IE", "Opera", "PhantomJS"] )
    parser.add_argument("-r", "--reports-dir", action="store",   dest="dir",      help="Directory to save screenshots.", default="reports")
    parser.add_argument('files', nargs='*')
    args = parser.parse_args()

    # Load files from the arguments.
    for filename in args.files:
        exec(open(filename).read())

    # See: http://codereview.stackexchange.com/q/88655/15346
    def make_suite(tc_class):
        testloader = unittest.TestLoader()
        testnames = testloader.getTestCaseNames(tc_class)
        suite = unittest.TestSuite()
        for name in testnames:
            suite.addTest(tc_class(name, cargs=args))
        return suite

    # Add all tests.
    alltests = unittest.TestSuite()
    for name, obj in inspect.getmembers(sys.modules[__name__]):
        if inspect.isclass(obj) and name.startswith("FooTest"):
            alltests.addTest(make_suite(obj))

    # Set-up logger
    verbose = bool(os.environ.get('VERBOSE', args.verbose))
    debug   = bool(os.environ.get('DEBUG', args.debug))
    if verbose or debug:
        logging.basicConfig( stream=sys.stdout )
        root = logging.getLogger()
        root.setLevel(logging.INFO if verbose else logging.DEBUG)
        ch = logging.StreamHandler(sys.stdout)
        ch.setLevel(logging.INFO if verbose else logging.DEBUG)
        ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s'))
        root.addHandler(ch)
    else:
        logging.basicConfig(stream=sys.stderr)

    # Run tests.
    result = unittest.TextTestRunner(verbosity=2).run(alltests)
    sys.exit(not result.wasSuccessful())

Для простоты прошу прощения за мои не- PEP8 стандарты кодирования.

Затем вы можете создать класс BaseTest для общих компонентов для всех ваших тестов, чтобы каждый ваш тест выглядел просто так:

from BaseTest import BaseTest
class FooTestPagesBasic(BaseTest):
    def test_foo(self):
        driver = self.driver
        driver.get(self.base_url + "/")

Для запуска вы просто указываете тесты как часть аргументов командной строки, например:

./run_tests.py -h http://example.com/ tests/**/*.py
kenorb
источник
2
большая часть этого ответа не имеет ничего общего с обнаружением тестов (т. е. ведением журнала и т. д.). Переполнение стека предназначено для ответов на вопросы, а не для демонстрации несвязанного кода.
Кори Голдберг