Как запустить все модульные тесты 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 строк. Надеюсь, другой начинающий питон сэкономит время, найдя это.


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)

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

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

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

2
большая часть этого ответа не имеет ничего общего с обнаружением тестов (т. е. ведением журнала и т. д.). Переполнение стека предназначено для ответов на вопросы, а не для демонстрации несвязанного кода.
— Кори Голдберг
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.