Як запустити всі тести модулів 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 unittest.


11
Проблеми включають: ImportError: Початковий каталог не можна перевезти:
Зінкінг

6
Принаймні, з Python 2.7.8 для Linux жоден виклик командного рядка не дає мені рекурсії. Мій проект має кілька підпроектів, одиничні тести яких перебувають у відповідних каталогах "unit_tests / <subproject> / python /". Якщо я вказую такий шлях, тестові одиниці для цього підпроекту виконуються, але з просто "unit_tests" як аргументом тестового каталогу ніяких тестів не знайдено (замість усіх тестів для всіх підпроектів, як я сподівався). Будь-який натяк?
user686249

6
Про рекурсію: Перша команда без <test_directory> за замовчуванням до ". і повторюється до субмодулів . Тобто всі тестові каталоги, які ви хочете виявити, повинні мати init .py. Якщо вони дійдуть, їх знайде команда виявлення. Просто спробував, спрацювало.
Еміль Стенстрем

Це працювало для мене. У мене є тестова папка з чотирма файлами, запустіть це з мого терміналу Linux, чудові речі.
JasTonAChair

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 (), що ви можете необов'язково оголосити тестовий набір в деяких модулях, а не в інших?
Corey Porter

1
Я спробував ніс, і він працює чудово. В моєму проекті було легко встановити та запустити. Мені навіть вдалося автоматизувати це за допомогою декількох рядків сценарію, запускаючи всередині virtualenv. +1 для носа!
Джессі Вебб

Це не завжди можливо: іноді імпортна структура проекту може призвести до заплутування носа, якщо він намагається запустити імпорт на модулях.
чиффа

4
Зауважте, що ніс був "в режимі обслуговування протягом останніх декількох років", і в даний час рекомендується використовувати nos2 , pytest або просто звичайний unittest / unittest2 для нових проектів.
Курт Пік

ви коли-небудь намагалися запустити тести з об’єкта тестового примірника?
Буратіно

96

У python 3, якщо ви використовуєте unittest.TestCase:

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

Потім ви можете запустити всі тести за допомогою:

python -m unittest

Готово! Розчин менше 100 рядків. Сподіваємось, інший новачок пітона економить час, знайшовши це.


3
Зауважте, що за замовчуванням він шукає лише тести у
файлах

3
Це правильно, оригінальне запитання стосувалося того, що "Кожен модульний тестовий модуль має форму тесту _ *. Py.", Тому ця відповідь у прямій відповіді. Зараз я оновив відповідь, щоб бути більш чітким
tmck-код

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Чому? Різниця, звідки вона походить?
simkus

У мене виникають проблеми із запуском файлу, який виглядає так у командному рядку. Як слід викликати це?
Дастін Міхельс

python file.py
slaughter98

1
Працювали бездоганно! Просто встановіть його у вашому тесті / dir, а потім встановіть start_id = "./". ІМХО, ця відповідь зараз (Python 3.7) прийнятий спосіб!
jjwdesign

Ви можете змінити останній рядок на ´res = runner.run (suite); sys.exit (0 якщо res.wasSuccessful () else 1) ´ якщо ви хочете правильний код виходу
Садап

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.


Від docs 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()функцію, він викликається і результат (який повинен бути а 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.