Jak uruchomić unittest Discover z „python setup.py test”?

82

Próbuję dowiedzieć się, jak python setup.py testuruchomić odpowiednik python -m unittest discover. Nie chcę używać skryptu run_tests.py i nie chcę używać żadnych zewnętrznych narzędzi testowych (takich jak noselub py.test). W porządku, jeśli rozwiązanie działa tylko w Pythonie 2.7.

W setup.py, myślę, że trzeba dodać coś do test_suitei / lub test_loaderpól w config, ale nie wydaje się znaleźć kombinację, która działa prawidłowo:

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

Czy jest to możliwe przy użyciu tylko unittestwbudowanego w Python 2.7?

FYI, struktura mojego projektu wygląda następująco:

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

Aktualizacja : Jest to możliwe z, unittest2ale chcę znaleźć coś równoważnego używając tylkounittest

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

unittest2 zawiera bardzo podstawowy kolektor testów kompatybilny z setuptools. Określ test_suite = 'unittest2.collector' w swoim setup.py. Rozpoczyna to wykrywanie testowe z domyślnymi parametrami z katalogu zawierającego setup.py, więc jest to prawdopodobnie najbardziej przydatne jako przykład (zobacz unittest2 / collector.py).

Na razie używam tylko skryptu o nazwie run_tests.py, ale mam nadzieję, że mogę się tego pozbyć, przechodząc do rozwiązania, które tylko używa python setup.py test.

Oto plik, run_tests.pyktóry mam nadzieję usunąć:

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)
cdwilson
źródło
Tylko słowo ostrzeżenia dla każdego, kto tu przyjedzie. Test setup.py jest uważany za „zapach” kodu i jest również ustawiony jako przestarzały. github.com/pytest-dev/pytest-runner/issues/50
Yashash Gaurav

Odpowiedzi:

44

Jeśli używasz py27 + lub py32 +, rozwiązanie jest dość proste:

test_suite="tests",
saschpe
źródło
1
Chciałbym, żeby to działało lepiej, natknąłem się na ten problem: stackoverflow.com/questions/6164004/… "Nazwy testów powinny pasować do nazw modułów. Jeśli istnieje test" foo_test.py ", musi istnieć odpowiedni moduł foo.py ”.
Charles L.
1
Zgadzam się. W moim przypadku, gdy testuję zewnętrzny Python, w którym dosłownie nie ma takiego modułu Pythona z .py, wydaje się, że nie ma dobrego sposobu na osiągnięcie tego.
Tom Swirly,
2
To jest poprawne rozwiązanie. Nie miałem problemu z @CharlesL. miał. Wszystkie moje testy są nazwane test_*.py. Ponadto dowiedziałem się, że faktycznie będzie rekurencyjnie przeszukiwać podany katalog, aby znaleźć każdą klasę, która rozszerza unittest.TestCast. Jest to niezwykle przydatne, jeśli masz strukturę katalogów, w której masz tests/first_batch/test_*.pyi tests/second_batch/test_*.py. Możesz po prostu określić, test_suite="tests",a wszystko będzie odbierane rekurencyjnie. Zauważ, że każdy zagnieżdżony katalog będzie musiał zawierać __init__.pyplik.
dcmm88,
39

Z tworzenia i dystrybucji pakietów za pomocą Setuptools (moje wyróżnienie ):

test_suite

Łańcuch określający podklasę unittest.TestCase (lub pakiet lub moduł zawierający co najmniej jedną z nich, albo metodę takiej podklasy) lub nazywający funkcję, która może być wywołana bez argumentów i zwraca unittest.TestSuite .

W związku setup.pyz tym dodałbyś funkcję, która zwraca TestSuite:

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

Następnie należy określić polecenie setupw następujący sposób:

setup(
    ...
    test_suite='setup.my_test_suite',
    ...
)
Michael G.
źródło
3
Jest problem z tym rozwiązaniem, ponieważ tworzy 2 „poziomy” unittest. Oznacza to, że setuptools utworzy polecenie 'test', które spróbuje utworzyć TestSuite z setup.my_test_suite, co zmusi go do zaimportowania setup.py, co spowoduje ponowne uruchomienie setup ()! Za drugim razem utworzy nowe (zagnieżdżone) polecenie testowe, które uruchomi żądany test. Może to nie być zauważalne dla większości ludzi, ale jeśli spróbujesz rozszerzyć polecenie testowe (musiałem je zmodyfikować, ponieważ nie mogę przeprowadzić testów „na miejscu”), możesz napotkać dziwne problemy. Zamiast tego użyj stackoverflow.com/a/21726329/3272850
dcmm88
2
To powoduje, że testy uruchamiają się dla mnie dwukrotnie z powodów wymienionych powyżej. Naprawiono to, przenosząc funkcję do __init__.pyfolderu testów i odnosząc się do tego.
Anonimowy
3
Problem z dwukrotnym wykonywaniem testów można łatwo naprawić, wykonując setup()funkcję wewnątrz if __name__ == '__main__':bloku w setup.pyskrypcie. Gdy skrypt instalacyjny jest wykonywany po raz pierwszy, zostanie wywołany blok if; za drugim razem skrypt instalacyjny zostanie zaimportowany jako moduł, więc blok if nie zostanie wywołany.
hoefling
Hmm, zdaję sobie sprawę, że mój plik setup.py NIE zawiera test_suitew ogóle tego parametru, ale „python setup.py test” nadal działa dobrze. Różni się to od tego, co mówi dokumentacja : „Jeśli nie ustawiłeś zestawu test_suite w wywołaniu setup () i nie podasz opcji --test-suite, wystąpi błąd”. Dowolny pomysł?
RayLuo,
21

Nie potrzebujesz konfiguracji, aby to działało. Zasadniczo można to zrobić na dwa główne sposoby:

Szybki sposób

Zmień nazwę test_module.pyna module_test.py(w zasadzie dodaj _testjako sufiks do testów dla konkretnego modułu), a python znajdzie go automatycznie. Po prostu dodaj to do setup.py:

from setuptools import setup, find_packages

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

Długa droga

Oto jak to zrobić z aktualną strukturą katalogów:

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

W obszarze tests/__init__.pychcesz zaimportować unittestskrypt testów jednostkowych test_modulei, a następnie utworzyć funkcję do uruchamiania testów. W tests/__init__.pywpisać w coś takiego:

import unittest
import test_module

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

TestLoaderKlasa ma inne funkcje oprócz loadTestsFromModule. Możesz pobiec, dir(unittest.TestLoader)aby zobaczyć pozostałe, ale ten jest najprostszy w użyciu.

Ponieważ twoja struktura katalogów jest taka, prawdopodobnie będziesz chciał, test_moduleaby móc zaimportować twój moduleskrypt. Być może już to zrobiłeś, ale na wypadek, gdybyś tego nie zrobił, możesz dołączyć ścieżkę nadrzędną, aby móc zaimportować packagemoduł i moduleskrypt. U góry test_module.pywpisz:

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

import unittest
import package.module
...

Na koniec setup.pydołącz testsmoduł i uruchom utworzone polecenie my_module_suite:

from setuptools import setup, find_packages

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

Wtedy po prostu biegniesz python setup.py test.

Oto próbka, którą ktoś podał jako odniesienie.

antymateria
źródło
2
Pytanie brzmiało, jak sprawić, by „python setup.py test” używał możliwości wykrywania unittest. To w ogóle tego nie dotyczy.
mikenerone
Ugh ... tak, całkowicie myślałem, że pytanie dotyczy czegoś innego. Nie jestem pewien, jak to się stało, chyba tracę zmysły :(
antymateria
5

Jednym z możliwych rozwiązań jest po prostu rozszerzenie testpolecenia dla distutilsi setuptools/ distribute. Wydaje się, że jest to totalna klęska i znacznie bardziej skomplikowana niż bym chciał, ale wydaje się, że poprawnie wykrywa i uruchamia wszystkie testy w moim pakiecie po uruchomieniu python setup.py test. Odkładam wybór tego jako odpowiedzi na moje pytanie w nadziei, że ktoś zaproponuje bardziej eleganckie rozwiązanie :)

(Zainspirowany https://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner )

Przykład 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)
cdwilson
źródło
3

Kolejne mniej niż idealne rozwiązanie, nieco inspirowane http://hg.python.org/unittest2/file/2b6411b9a838/unittest2/collector.py

Dodaj moduł, który zwraca TestSuiteliczbę wykrytych testów. Następnie skonfiguruj Instalatora, aby wywołać ten moduł.

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

Oto 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)

A oto 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)
cdwilson
źródło
3

unittestModuł biblioteki standardowej Pythona obsługuje wykrywanie (w Pythonie 2.7 i nowszych oraz Python 3.2 i nowszych). Jeśli możesz założyć te minimalne wersje, możesz po prostu dodać discoverargument wiersza poleceń do unittestpolecenia.

Wystarczy niewielka poprawka, aby 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,
    },
)
mikenerone
źródło
BTW, zakładam powyżej, że celujesz tylko w wersje Pythona, które faktycznie obsługują wykrywanie (2.7 i 3.2+), ponieważ pytanie dotyczy konkretnie tej funkcji. Możesz oczywiście owinąć wkładkę w sprawdzanie wersji, jeśli chcesz zachować zgodność ze starszymi wersjami (w ten sposób używając standardowego modułu ładującego setuptools w takich przypadkach).
mikenerone
0

Nie usunie to run_tests.py, ale sprawi, że będzie działać z setuptools. Dodaj:

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

Następnie w setup.py: (zakładam, że robisz coś takiego setup(**config))

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

Jedynym minusem, jaki widzę, jest naginanie semantyki loadTestsFromNames, ale polecenie testowe setuptools jest jedynym konsumentem i wywołuje je w określony sposób .

jwelsh
źródło