Як запустити unittest Discover за допомогою “python setup.py test”?


82

Я намагаюся зрозуміти, як python setup.py testзапустити еквівалент python -m unittest discover. Я не хочу використовувати сценарій run_tests.py, і я не хочу використовувати будь-які зовнішні інструменти тестування (наприклад, noseабо py.test). Це нормально, якщо рішення працює лише на python 2.7.

У setup.py, я думаю, мені потрібно щось додати до полів test_suiteта / або test_loaderв конфігурації, але я не можу знайти комбінацію, яка працює правильно:

config = {
    'name': name,
    'version': version,
    'url': url,
    'test_suite': '???',
    'test_loader': '???',
}

Чи можливо це лише за допомогою unittestвбудованого в python 2.7?

FYI, моя структура проекту виглядає так:

project/
  package/
    __init__.py
    module.py
  tests/
    __init__.py
    test_module.py
  run_tests.py <- I want to delete this
  setup.py

Оновлення : це можливо за допомогоюunittest2 але я хочу знайти щось еквівалентне, використовуючи лишеunittest

З https://pypi.python.org/pypi/unittest2

unittest2 включає дуже базовий збирач тестів, сумісний з setuptools. Вкажіть test_suite = 'unittest2.collector' у вашому setup.py. Це запускає тестове виявлення з параметрів за замовчуванням із каталогу, що містить setup.py, тому, мабуть, це найкорисніший приклад (див. Unittest2 / collector.py).

На даний момент я просто використовую скрипт, який називається run_tests.py, але я сподіваюся, що зможу позбутися цього, перейшовши до рішення, яке лише використовує python setup.py test.

Ось те, що run_tests.pyя сподіваюся видалити:

import unittest

if __name__ == '__main__':

    # use the default shared TestLoader instance
    test_loader = unittest.defaultTestLoader

    # use the basic test runner that outputs to sys.stderr
    test_runner = unittest.TextTestRunner()

    # automatically discover all tests in the current dir of the form test*.py
    # NOTE: only works for python 2.7 and later
    test_suite = test_loader.discover('.')

    # run the test suite
    test_runner.run(test_suite)

Лише слово обережності кожному, хто випадково завітає сюди. Тест setup.py вважається кодом "запаху", а також встановлюється як застарілий. github.com/pytest-dev/pytest-runner/issues/50
Яшаш Гаурав,

Відповіді:


44

Якщо ви використовуєте py27 + або py32 +, рішення досить просте:

test_suite="tests",

1
Я хотів би, щоб це працювало краще, я зіткнувся з цією проблемою: stackoverflow.com/questions/6164004/… "Імена тестів повинні відповідати іменам модулів. Якщо існує тест" foo_test.py ", повинен бути відповідний модуль foo.py . "
Чарльз Л.

1
Я згоден. У моєму випадку, коли я тестую зовнішній Python, де буквально немає такого модуля Python з .py, здається, немає хорошого способу досягти цього.
Том Свірі

2
Це правильне рішення. У мене не було випуску @CharlesL. мали. Усі мої тести названі test_*.py. Крім того, я з’ясував, що він насправді буде здійснювати рекурсивний пошук у даному каталозі, щоб знайти будь-який клас, який розширюється unittest.TestCast. Це надзвичайно корисно, якщо у вас є структура каталогів, де у вас є tests/first_batch/test_*.pyі tests/second_batch/test_*.py. Ви можете просто вказати, test_suite="tests",і він підбере все рекурсивно. Зверніть увагу, що в кожному вкладеному каталозі повинен бути __init__.pyфайл.
dcmm88

39

З побудови та розповсюдження пакетів за допомогою Setuptools (наголос на моєму):

test_suite

Рядок, що називає підклас unittest.TestCase (або пакет або модуль, що містить один або кілька з них, або метод такого підкласу), або називає функцію, яку можна викликати без аргументів, і повертає unittest.TestSuite .

Отже, до setup.pyвас слід додати функцію, яка повертає TestSuite:

import unittest
def my_test_suite():
    test_loader = unittest.TestLoader()
    test_suite = test_loader.discover('tests', pattern='test_*.py')
    return test_suite

Потім ви вказали б команду setupнаступним чином:

setup(
    ...
    test_suite='setup.my_test_suite',
    ...
)

3
Існує проблема з цим рішенням, оскільки воно створює 2 "рівні" unittest. Це означає, що setuptools створить команду 'test', яка спробує створити TestSuite з setup.my_test_suite, що змусить його імпортувати setup.py, який знову запустить setup ()! Цього разу він створить нову (вкладену) тестову команду, яка запускає бажаний тест. Це може бути не помітним для більшості людей, але якщо ви спробуєте розширити команду тесту (мені потрібно було її змінити, оскільки я не можу запустити тести на місці), у вас можуть виникнути дивні проблеми. Використання stackoverflow.com/a/21726329/3272850 замість
dcmm88

2
Це змушує тести запускатись двічі для мене із зазначених вище причин. Виправлено, перемістивши функцію в __init__.pyпапку тестів і посилаючись на неї.
Анонім

3
Проблему з тестами, що виконуються двічі, можна легко вирішити, виконавши setup()функцію всередині if __name__ == '__main__':блоку в setup.pyсценарії. Перший раз, коли виконується сценарій установки, тому буде викликаний блок if; вдруге сценарій установки буде імпортовано як модуль, тому блок if не буде викликаний.
hoefling

Хм, я розумію, що мій файл setup.py НЕ містить цього test_suite параметра взагалі, проте "тест python setup.py" все ще працює для мене добре. Це відрізняється від того, що сказано в документації : "Якщо ви не встановили test_suite у своєму виклику setup () і не надаєте опцію --test-suite, трапиться помилка." Будь-яка ідея?
RayLuo

21

Вам не потрібна конфігурація, щоб це працювало. Існує два основних способи зробити це:

Швидкий шлях

Перейменуйте ваш test_module.pyна module_test.py(в основному додайте _testяк суфікс до тестів для певного модуля), і python знайде його автоматично. Просто не забудьте додати це до setup.py:

from setuptools import setup, find_packages

setup(
    ...
    test_suite = 'tests',
    ...
)

Довгий шлях

Ось як це зробити за вашої поточної структури каталогів:

project/
  package/
    __init__.py
    module.py
  tests/
    __init__.py
    test_module.py
  run_tests.py <- I want to delete this
  setup.py

У розділі tests/__init__.pyви хочете імпортувати unittestскрипт test_moduleі модульний тест , а потім створити функцію для запуску тестів. В tests/__init__.py, введіть у чому - щось на зразок цього:

import unittest
import test_module

def my_module_suite():
    loader = unittest.TestLoader()
    suite = loader.loadTestsFromModule(test_module)
    return suite

TestLoaderКлас має інші функції , крім loadTestsFromModule. Ви можете бігти, dir(unittest.TestLoader)щоб побачити інші, але цей найпростіший у використанні.

Оскільки ваша структура каталогів така, ви, ймовірно, захочете test_moduleмати можливість імпортувати вашmodule сценарій. Можливо, ви вже це зробили, але на випадок, якщо цього не зробили, ви можете включити батьківський шлях, щоб можна було імпортувати packageмодуль та moduleсценарій. У верхній частині test_module.py, введіть:

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

import unittest
import package.module
...

Тоді нарешті, в setup.py включіть testsмодуль і запустіть створену вами команду my_module_suite:

from setuptools import setup, find_packages

setup(
    ...
    test_suite = 'tests.my_module_suite',
    ...
)

Тоді ти просто біжиш python setup.py test.

Ось зразок, який хтось зробив як посилання.


2
Питання полягало в тому, як зробити так, щоб "python setup.py test" використовував можливість виявлення unittest. Це взагалі не стосується цього.
mikenerone

Тьфу ... так, я цілком думав, що питання ставить щось інше. Я не впевнений, як це сталося, мабуть, я втрачаю розум :(
антиматерія

5

Одне з можливих рішень - просто розширити testкоманду для distutilsта setuptools/distribute . Це здається загальним клубом і набагато складнішим, ніж я б вважав за краще, але, схоже, правильно виявляю і запускаю всі тести в моєму пакеті після запускуpython setup.py test . Я затримуюсь, вибравши це як відповідь на своє запитання, сподіваючись, що хтось запропонує більш елегантне рішення :)

(Натхненно https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner )

Приклад setup.py:

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

def discover_and_run_tests():
    import os
    import sys
    import unittest

    # get setup.py directory
    setup_file = sys.modules['__main__'].__file__
    setup_dir = os.path.abspath(os.path.dirname(setup_file))

    # use the default shared TestLoader instance
    test_loader = unittest.defaultTestLoader

    # use the basic test runner that outputs to sys.stderr
    test_runner = unittest.TextTestRunner()

    # automatically discover all tests
    # NOTE: only works for python 2.7 and later
    test_suite = test_loader.discover(setup_dir)

    # run the test suite
    test_runner.run(test_suite)

try:
    from setuptools.command.test import test

    class DiscoverTest(test):

        def finalize_options(self):
            test.finalize_options(self)
            self.test_args = []
            self.test_suite = True

        def run_tests(self):
            discover_and_run_tests()

except ImportError:
    from distutils.core import Command

    class DiscoverTest(Command):
        user_options = []

        def initialize_options(self):
                pass

        def finalize_options(self):
            pass

        def run(self):
            discover_and_run_tests()

config = {
    'name': 'name',
    'version': 'version',
    'url': 'http://example.com',
    'cmdclass': {'test': DiscoverTest},
}

setup(**config)

3

Ще одне менш ідеальне рішення, злегка натхнене http://hg.python.org/unittest2/file/2b6411b9a838/unittest2/collector.py

Додайте модуль, який повертає TestSuiteвиявлені тести. Потім налаштуйте налаштування для виклику цього модуля.

project/
  package/
    __init__.py
    module.py
  tests/
    __init__.py
    test_module.py
  discover_tests.py
  setup.py

Ось discover_tests.py:

import os
import sys
import unittest

def additional_tests():
    setup_file = sys.modules['__main__'].__file__
    setup_dir = os.path.abspath(os.path.dirname(setup_file))
    return unittest.defaultTestLoader.discover(setup_dir)

І ось setup.py:

try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

config = {
    'name': 'name',
    'version': 'version',
    'url': 'http://example.com',
    'test_suite': 'discover_tests',
}

setup(**config)

3

Стандартний бібліотечний unittestмодуль Python підтримує виявлення (у Python 2.7 та пізніших версіях та Python 3.2 та новіших версіях). Якщо ви можете припустити ці мінімальні версії, ви можете просто додати discoverаргумент командного рядка до unittestкоманди.

Потрібна лише невелика настройка, щоб setup.py:

import setuptools.command.test
from setuptools import (find_packages, setup)

class TestCommand(setuptools.command.test.test):
    """ Setuptools test command explicitly using test discovery. """

    def _test_args(self):
        yield 'discover'
        for arg in super(TestCommand, self)._test_args():
            yield arg

setup(
    ...
    cmdclass={
        'test': TestCommand,
    },
)

До речі, я припускаю вище, що ви націлюєтесь лише на версії Python, які фактично підтримують виявлення (2.7 та 3.2+), оскільки питання стосується саме цієї функції. Звичайно, ви можете обернути вставку у перевірку версій, якщо ви хочете також залишатися сумісним зі старими версіями (таким чином, використовуючи стандартний завантажувач setuptools у цих випадках).
mikenerone

0

Це не видалить run_tests.py, але змусить його працювати з setuptools. Додати:

class Loader(unittest.TestLoader):
    def loadTestsFromNames(self, names, _=None):
        return self.discover(names[0])

Потім у setup.py: (припускаю, ви робите щось на зразок setup(**config))

config = {
    ...
    'test_loader': 'run_tests:Loader',
    'test_suite': '.', # your start_dir for discover()
}

Єдиний мінус, який я бачу, - це згинання семантики loadTestsFromNames, але команда тесту setuptools є єдиним споживачем і викликає його вказаним чином .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.