Запуск unittest с типичной тестовой структурой каталогов

702

Кажется, что очень распространенная структура каталогов даже для простого модуля Python - это разделение модульных тестов на их собственный testкаталог:

new_project/
    antigravity/
        antigravity.py
    test/
        test_antigravity.py
    setup.py
    etc.

например, посмотрите это руководство по проекту Python .

У меня вопрос просто : как обычно проводить тесты? Я подозреваю, что это очевидно для всех, кроме меня, но вы не можете просто запустить python test_antigravity.pyиз тестового каталога, так как import antigravityон потерпит неудачу, так как модуль не находится на пути.

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

Другая альтернатива - просто скопировать тестовый файл в другой каталог, но он выглядит немного глупым и упускает смысл начинать их с отдельного каталога.

Итак, если бы вы только что загрузили исходный код в мой новый проект, как бы вы запустили модульные тесты? Я бы предпочел ответ, который позволил бы мне сказать моим пользователям: «Чтобы запустить модульные тесты, сделайте X».

Майор майор
источник
5
@EMP Правильное решение, когда вам нужно установить путь поиска, это ... установить путь поиска. Какого решения вы ожидали?
Карл Мейер
7
@CarlMeyer еще одно лучшее решение - использовать unittestинтерфейс командной строки, как описано в моем ответе ниже, чтобы вам не нужно было добавлять каталог в путь.
Пьер
13
Тоже самое. Я только начал писать свои самые первые модульные тесты для крошечного проекта Python и несколько дней пытался рассуждать о том, что я не могу легко запустить тест, сохраняя мои исходники в каталоге src, а тесты - в каталоге test, казалось бы, с любой из существующих тестовых структур. Я в конце концов приму вещи, придумаю способ; но это было очень разочаровывающее введение. (А я ветеран юнит-тестирования за пределами Python.)
Ates Goral

Ответы:

657

Лучшее решение, на мой взгляд, это использовать unittest интерфейс командной строки, который добавит каталог, sys.pathтак что вам не нужно (сделано в TestLoaderклассе).

Например, для такой структуры каталогов:

new_project
├── antigravity.py
└── test_antigravity.py

Вы можете просто запустить:

$ cd new_project
$ python -m unittest test_antigravity

Для структуры каталогов, подобных вашей:

new_project
├── antigravity
   ├── __init__.py         # make it a package
   └── antigravity.py
└── test
    ├── __init__.py         # also make test a package
    └── test_antigravity.py

И в тестовых модулях внутри testпакета вы можете импортировать antigravityпакет и его модули как обычно:

# import the package
import antigravity

# import the antigravity module
from antigravity import antigravity

# or an object inside the antigravity module
from antigravity.antigravity import my_object

Запуск одного тестового модуля:

Чтобы запустить один тестовый модуль, в этом случае test_antigravity.py:

$ cd new_project
$ python -m unittest test.test_antigravity

Просто ссылайтесь на тестовый модуль так же, как вы импортируете его.

Выполнение одного теста или метода тестирования:

Также вы можете запустить один TestCaseили один метод тестирования:

$ python -m unittest test.test_antigravity.GravityTestCase
$ python -m unittest test.test_antigravity.GravityTestCase.test_method

Запуск всех тестов:

Вы также можете использовать тестовое обнаружение, которое обнаружит и запустит все тесты для вас, это должны быть модули или пакеты с именами test*.py(можно изменить с помощью -p, --patternфлага):

$ cd new_project
$ python -m unittest discover
$ # Also works without discover for Python 3
$ # as suggested by @Burrito in the comments
$ python -m unittest

Это запустит все test*.pyмодули внутри testпакета.

пьер
источник
53
python -m unittest discoverнайдет и запустит тесты в testкаталоге, если они названы test*.py. Если вы назвали подкаталог tests, используйте python -m unittest discover -s tests, и если вы назвали тестовые файлы antigravity_test.py, используйте python -m unittest discover -s tests -p '*test.py' Имена файлов, можно использовать подчеркивания, но не тире.
Mike3d0g
10
Это терпит неудачу для меня на Python 3 с ошибкой ImportError: No module named 'test.test_antigravity'из-за конфликта с тестовым подмодулем библиотеки unittest. Возможно, эксперт может подтвердить и изменить имя подкаталога ответа, например, «тесты» (множественное число).
expz
10
Мои test_antigravity.pyвсе еще выдает ошибку импорта для обоих import antigravityи from antigravity import antigravity, как хорошо. У меня есть оба __init_.pyфайла, и я звоню python3 -m unittest discoverиз new projectкаталога. Что еще может быть не так?
imrek
20
файл test/__init__.pyимеет решающее значение здесь, даже если он пуст
Франсуа
3
@ Mike3d0g не уверен, что вы имели в виду, что имя каталога testявляется специальным ... но для справки , это не так. : P python -m unittest discoverработает с тестовыми файлами tests/так же хорошо, как и test/.
Райан
49

Самое простое решение для ваших пользователей - предоставить исполняемый скрипт ( runtests.pyили какой-то другой), который загружает необходимую тестовую среду, включая, при необходимости, sys.pathвременное добавление корневого каталога проекта . Это не требует, чтобы пользователи устанавливали переменные окружения, что-то вроде этого прекрасно работает в скрипте начальной загрузки:

import sys, os

sys.path.insert(0, os.path.dirname(__file__))

Тогда ваши инструкции для пользователей могут быть такими же простыми, как "python runtests.py ".

Конечно, если путь, который вам действительно нужен os.path.dirname(__file__), то вам вообще не нужно его добавлять sys.path; Python всегда помещает каталог запущенного в данный момент скрипта в начало sys.path, поэтому в зависимости от вашей структуры каталогов, просто найти runtests.pyнужное место может быть все, что нужно.

Кроме того, модуль unittest в Python 2.7+ (который перенесен как unittest2 для Python 2.6 и более ранних версий ) теперь имеет встроенное обнаружение тестов , поэтому в носе больше нет необходимости, если вы хотите автоматическое обнаружение тестов: ваши пользовательские инструкции могут быть такими же простыми, как python -m unittest discover,

Карл Мейер
источник
Я поместил некоторые тесты в подпапку типа «Major Major». Они могут работать с Python -m Unittest Discover, но как выбрать только один из них? Если я запускаю python -m unittest tests / testxxxxx, то он завершается ошибкой из-за проблем с путями. Поскольку режим dicovery решает все проблемы, я ожидаю, что есть еще один прием для решения проблемы с путями без исправления путем ручного кодирования, который вы предлагаете в первой точке
Фредерик Базин
2
@FredericBazin Не используйте обнаружение, если вам нужен только один тест или тестовый файл, просто назовите модуль, который вы хотите запустить. Если вы назовете его как пунктирный путь модуля (а не путь к файлу), он сможет правильно определить путь поиска. Смотрите Питер ответ для более подробной информации.
Карл Мейер
Этот хак был полезен в сценарии, где я должен был запустить что-то вроде python -m pdb tests\test_antigravity.py. Внутри pdb я выполнил, sys.path.insert(0, "antigravity")что позволило оператору импорта разрешить, как будто я запускаю модуль.
ixe013
23

Обычно я создаю скрипт «run tests» в каталоге проекта (тот, который является общим как для исходного каталога, так и для test), который загружает мой «All Tests» комплект. Обычно это стандартный код, поэтому я могу использовать его от проекта к проекту.

run_tests.py:

import unittest
import test.all_tests
testSuite = test.all_tests.create_test_suite()
text_runner = unittest.TextTestRunner().run(testSuite)

test / all_tests.py (из Как запустить все модульные тесты Python в каталоге? )

import glob
import unittest

def create_test_suite():
    test_file_strings = glob.glob('test/test_*.py')
    module_strings = ['test.'+str[5:len(str)-3] for str in test_file_strings]
    suites = [unittest.defaultTestLoader.loadTestsFromName(name) \
              for name in module_strings]
    testSuite = unittest.TestSuite(suites)
    return testSuite

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

stw_dev
источник
1
Я также хотел в run testsсценарий в директории проекта и нашел более чистый способ много , чтобы сделать это. Настоятельно рекомендуется.
z33k
18

Из статьи, на которую вы ссылаетесь:

Создайте файл test_modulename.py и поместите в него свои юнит-тесты. Поскольку тестовые модули находятся в отдельном каталоге от вашего кода, вам может потребоваться добавить родительский каталог вашего модуля в PYTHONPATH, чтобы запустить их:

$ cd /path/to/googlemaps

$ export PYTHONPATH=$PYTHONPATH:/path/to/googlemaps/googlemaps

$ python test/test_googlemaps.py

Наконец, есть еще одна популярная среда модульного тестирования для Python (это так важно!), Нос. Нос помогает упростить и расширить встроенную инфраструктуру unittest (например, он может автоматически найти ваш тестовый код и настроить PYTHONPATH для вас), но он не включен в стандартный дистрибутив Python.

Возможно, вы должны смотреть на нос, как он предлагает?

Марк Байерс
источник
3
Да, это работает (для меня), но я действительно прошу простейших инструкций, которые я могу дать пользователям моего модуля, чтобы заставить их запускать тесты. Модификация пути могла бы быть на самом деле, но я ловлю что-то более прямолинейное.
майор
4
Так как же выглядит ваш путь к Python после того, как вы поработали над сотней проектов? Я должен вручную войти и очистить мой путь? Если это так, то это отвратительный дизайн!
jeremyjjbrown
11

У меня была такая же проблема, с отдельной папкой юнит-тестов. Из упомянутых предложений я добавляю абсолютный исходный путь кsys.path .

Преимущество следующего решения заключается в том, что файл можно запустить, test/test_yourmodule.pyне переходя сначала в каталог test:

import sys, os
testdir = os.path.dirname(__file__)
srcdir = '../antigravity'
sys.path.insert(0, os.path.abspath(os.path.join(testdir, srcdir)))

import antigravity
import unittest
andpei
источник
9

если вы запустите "python setup.py development", то пакет будет в пути. Но вы можете не захотеть этого делать, потому что вы можете заразить вашу системную установку python, поэтому существуют такие инструменты, как virtualenv и buildout .

Том Уиллис
источник
7

Решение / Пример для модуля Python unittest

Учитывая следующую структуру проекта:

ProjectName
 ├── project_name
 |    ├── models
 |    |    └── thing_1.py
 |    └── __main__.py
 └── test
      ├── models
      |    └── test_thing_1.py
      └── __main__.py

Вы можете запустить свой проект из корневого каталога с помощью python project_nameкоторого вызываются ProjectName/project_name/__main__.py.


Чтобы запустить ваши тесты с python testэффективным запуском ProjectName/test/__main__.py, вам нужно сделать следующее:

1) Преврати свой test/modelsкаталог в пакет, добавив __init__.pyфайл. Это делает тестовые случаи в подкаталоге доступными из родительского testкаталога.

# ProjectName/test/models/__init__.py

from .test_thing_1 import Thing1TestCase        

2) Измените системный путь, test/__main__.pyчтобы включить project_nameкаталог.

# ProjectName/test/__main__.py

import sys
import unittest

sys.path.append('../project_name')

loader = unittest.TestLoader()
testSuite = loader.discover('test')
testRunner = unittest.TextTestRunner(verbosity=2)
testRunner.run(testSuite)

Теперь вы можете успешно импортировать вещи из project_nameваших тестов.

# ProjectName/test/models/test_thing_1.py    

import unittest
from project_name.models import Thing1  # this doesn't work without 'sys.path.append' per step 2 above

class Thing1TestCase(unittest.TestCase):

    def test_thing_1_init(self):
        thing_id = 'ABC'
        thing1 = Thing1(thing_id)
        self.assertEqual(thing_id, thing.id)
Дерек Соике
источник
5

Используйте, setup.py developчтобы ваш рабочий каталог был частью установленной среды Python, затем запустите тесты.

Нед Бэтчелдер
источник
Это дает мне, invalid command 'develop'и эта опция не упоминается, если я попрошу setup.py --help-commands. Нужно ли что-то в setup.pyсебе, чтобы это работало?
майор
Все в порядке - проблема в том, что я пропустил файл import setuptoolsиз моего setup.pyфайла. Но я думаю, это показывает, что это не будет работать все время для модулей других людей.
майор
1
Если у вас есть pip , вы можете использовать его для установки вашего пакета в «редактируемом» режиме : pip install -e .это также добавляет пакет в среду Python без копирования исходного кода, что позволяет вам продолжать редактировать его там, где он находится.
Эрик Смит
pip install -e .это то же самое python setup.py develop, что просто обезьянаsetup.py к использованию setuptools, даже если это не так, поэтому он работает в любом случае.
Карл Мейер
5

Если вы используете VS Code и ваши тесты расположены на том же уровне, что и ваш проект, то запуск и отладка вашего кода не будут работать из коробки. Что вы можете сделать, это изменить файл launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python",
            "type": "python",
            "request": "launch",
            "stopOnEntry": false,
            "pythonPath": "${config:python.pythonPath}",
            "program": "${file}",
            "cwd": "${workspaceRoot}",
            "env": {},
            "envFile": "${workspaceRoot}/.env",
            "debugOptions": [
                "WaitOnAbnormalExit",
                "WaitOnNormalExit",
                "RedirectOutput"
            ]
        }    
    ]
}

Ключевая строка здесь envFile

"envFile": "${workspaceRoot}/.env",

В корне вашего проекта добавьте .env файл

Внутри вашего .env файла добавьте путь к корню вашего проекта. Это временно добавит

PYTHONPATH = C: \ ВАШЕГО \ ПИТОНА \ PROJECT \ ROOT_DIRECTORY

путь к вашему проекту, и вы сможете использовать отладочные юнит-тесты из VS Code

Влад Безден
источник
5

Я заметил, что если вы запускаете интерфейс командной строки unittest из своего каталога "src", то импорт работает корректно без изменений.

python -m unittest discover -s ../test

Если вы хотите поместить это в командный файл в каталоге вашего проекта, вы можете сделать это:

setlocal & cd src & python -m unittest discover -s ../test
Алан Л
источник
5

У меня была такая же проблема в течение длительного времени. Недавно я выбрал следующую структуру каталогов:

project_path
├── Makefile
├── src
   ├── script_1.py
   ├── script_2.py
   └── script_3.py
└── tests
    ├── __init__.py
    ├── test_script_1.py
    ├── test_script_2.py
    └── test_script_3.py

и в __init__.pyскрипте тестовой папки я пишу следующее:

import os
import sys
PROJECT_PATH = os.getcwd()
SOURCE_PATH = os.path.join(
    PROJECT_PATH,"src"
)
sys.path.append(SOURCE_PATH)

Очень важным для совместного использования проекта является Makefile, потому что он обеспечивает правильную работу скриптов. Вот команда, которую я положил в Makefile:

run_tests:
    python -m unittest discover .

Makefile важен не только из-за команды, которую он запускает, но и из-за того, откуда он его запускает . Если бы вы тестировали компакт-диск и делали это python -m unittest discover ., он бы не работал, потому что инициализация скрипт в unit_tests вызывает os.getcwd (), который затем указывал бы на неверный абсолютный путь (который будет добавлен в sys.path и вы пропустите ваша исходная папка). Сценарии запускались бы, так как Discover находит все тесты, но они не работают должным образом. Таким образом, Makefile существует, чтобы избежать необходимости помнить эту проблему.

Мне действительно нравится этот подход, потому что мне не нужно трогать мою папку src, мои модульные тесты или переменные окружения, и все идет гладко.

Дайте мне знать, если вам это нравится.

Надеюсь, это поможет,

Патрик Да Силва
источник
4

Ниже приведена структура моего проекта:

ProjectFolder:
 - project:
     - __init__.py
     - item.py
 - tests:
     - test_item.py

Я нашел, что лучше импортировать в метод setUp ():

import unittest
import sys    

class ItemTest(unittest.TestCase):

    def setUp(self):
        sys.path.insert(0, "../project")
        from project import item
        # further setup using this import

    def test_item_props(self):
        # do my assertions

if __name__ == "__main__":
    unittest.main()
rolika
источник
4

Каков обычный способ выполнения тестов?

Я использую Python 3.6.2

cd new_project

pytest test/test_antigravity.py

Чтобы установить pytest :sudo pip install pytest

Я не установил никакой переменной пути, и мой импорт не завершился с той же «тестовой» структурой проекта.

Я закомментировал этот материал: if __name__ == '__main__'как это:

test_antigravity.py

import antigravity

class TestAntigravity(unittest.TestCase):

    def test_something(self):

        # ... test stuff here


# if __name__ == '__main__':
# 
#     if __package__ is None:
# 
#         import something
#         sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
#         from .. import antigravity
# 
#     else:
# 
#         from .. import antigravity
# 
#     unittest.main()
aliopi
источник
4

Можно использовать упаковщик, который запускает выбранные или все тесты.

Например:

./run_tests antigravity/*.py

или для запуска всех тестов рекурсивно используйте globbing ( tests/**/*.py) (включитьshopt -s globstar ).

Оболочка может в основном использовать argparseдля анализа аргументов, таких как:

parser = argparse.ArgumentParser()
parser.add_argument('files', nargs='*')

Затем загрузите все тесты:

for filename in args.files:
    exec(open(filename).read())

затем добавьте их в свой набор тестов (используя inspect):

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

и запустить их:

result = unittest.TextTestRunner(verbosity=2).run(alltests)

Проверьте этот пример для более подробной информации.

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

kenorb
источник
4

Python 3+

Добавление в @Pierre

Используя unittestструктуру каталогов следующим образом:

new_project
├── antigravity
   ├── __init__.py         # make it a package
   └── antigravity.py
└── test
    ├── __init__.py         # also make test a package
    └── test_antigravity.py

Чтобы запустить тестовый модуль test_antigravity.py:

$ cd new_project
$ python -m unittest test.test_antigravity

Или один TestCase

$ python -m unittest test.test_antigravity.GravityTestCase

Обязательно не забывайте, __init__.pyдаже если пусто, иначе не сработает.

eusoubrasileiro
источник
2

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

Сначала создайте файл test / context.py со следующим содержимым:

import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

Затем выполните следующий импорт в файл test / test_antigravity.py:

import unittest
try:
    import context
except ModuleNotFoundError:
    import test.context    
import antigravity

Обратите внимание на то, что причина этого предложения о том, что

  • сбой импорта test.context при запуске с «python test_antigravity.py» и
  • Сбой импорта контекста при запуске с "python -m unittest" из каталога new_project.

С этим обманом они оба работают.

Теперь вы можете запустить все тестовые файлы в тестовом каталоге с помощью:

$ pwd
/projects/new_project
$ python -m unittest

или запустите отдельный тестовый файл с:

$ cd test
$ python test_antigravity

Хорошо, это не намного красивее, чем содержание context.py в test_antigravity.py, но, возможно, немного. Предложения приветствуются.

TJK
источник
2

Если у вас есть несколько каталогов в вашем тестовом каталоге, то вы должны добавить в каждый каталог __init__.pyфайл.

/home/johndoe/snakeoil
└── test
    ├── __init__.py        
    └── frontend
        └── __init__.py
        └── test_foo.py
    └── backend
        └── __init__.py
        └── test_bar.py

Затем, чтобы запустить каждый тест сразу, запустите:

python -m unittest discover -s /home/johndoe/snakeoil/test -t /home/johndoe/snakeoil

Источник: python -m unittest -h

  -s START, --start-directory START
                        Directory to start discovery ('.' default)
  -t TOP, --top-level-directory TOP
                        Top level directory of project (defaults to start
                        directory)
Qlimax
источник
1

Этот BASH-скрипт будет выполнять тестовый каталог python unittest из любой точки файловой системы, независимо от того, в каком рабочем каталоге вы находитесь.

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

#!/bin/bash

this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

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

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

Джон Грин
источник
1

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

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

import sys, os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))

Добавьте это в начало всех ваших тестовых сценариев. Это добавит основную папку проекта в системный путь, поэтому любой импорт модулей, который работает оттуда, теперь будет работать. И не важно, откуда вы запускаете тесты.

Очевидно, вы можете изменить файл project_path_hack, чтобы он соответствовал местоположению вашей основной папки проекта.

chasmani
источник
0

Если вы ищете решение только для командной строки:

На основе следующей структуры каталогов (обобщенной с выделенным каталогом источника):

new_project/
    src/
        antigravity.py
    test/
        test_antigravity.py

Windows : (в new_project)

$ set PYTHONPATH=%PYTHONPATH%;%cd%\src
$ python -m unittest discover -s test

Посмотрите этот вопрос, если вы хотите использовать его в цикле for.

Linux : (в new_project)

$ export PYTHONPATH=$PYTHONPATH:$(pwd)/src  [I think - please edit this answer if you are a Linux user and you know this]
$ python -m unittest discover -s test

При таком подходе также возможно добавить дополнительные каталоги в PYTHONPATH, если это необходимо.

pj.dewitte
источник
0

Вы должны действительно использовать инструмент pip.

Используйте pip install -e .для установки вашего пакета в режиме разработки. Это очень хорошая практика, рекомендованная pytest (см. Документацию по хорошей практике , где вы также можете найти два макета проекта для подражания).

Кальмар
источник
Зачем занижать этот ответ? Я прочитал принятый ответ, и хотя он не был плохим, pytestлучше запускать тесты, потому что вывод на консоль цветной, с информацией о трассировке стека и подробной информацией об ошибках подтверждения.
Алиопи