Wie führe ich unittest remove aus "python setup.py test" aus?

80

Ich versuche herauszufinden, wie ich python setup.py testdas Äquivalent von ausführen kann python -m unittest discover. Ich möchte kein Skript run_tests.py verwenden und keine externen Testtools (wie noseoder py.test) verwenden. Es ist in Ordnung, wenn die Lösung nur unter Python 2.7 funktioniert.

In setup.py, ich glaube ich etwas zu den hinzufügen müssen test_suiteund / oder test_loaderFelder in Config, aber ich kann scheinen nicht eine Kombination zu finden , die richtig funktioniert:

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

Ist dies nur unittestmit Python 2.7 möglich?

Zu Ihrer Information, meine Projektstruktur sieht folgendermaßen aus:

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

Update : Dies ist möglich mit, unittest2aber ich möchte etwas Äquivalentes nur mit findenunittest

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

unittest2 enthält einen sehr einfachen Setuptools-kompatiblen Testkollektor. Geben Sie test_suite = 'unittest2.collector' in Ihrer setup.py an. Dies startet die Testerkennung mit den Standardparametern aus dem Verzeichnis, das setup.py enthält. Daher ist dies möglicherweise am nützlichsten (siehe unittest2 / collector.py).

Im Moment verwende ich nur ein Skript namens run_tests.py, aber ich hoffe, dass ich dies beseitigen kann, indem ich zu einer Lösung übergehe, die nur verwendet python setup.py test.

Hier ist das, was run_tests.pyich entfernen möchte:

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
quelle
Nur ein Wort der Vorsicht an alle, die zufällig hierher kommen. Der setup.py-Test wird als Code-Geruch betrachtet und ist ebenfalls veraltet. github.com/pytest-dev/pytest-runner/issues/50
Yashash Gaurav

Antworten:

44

Wenn Sie py27 + oder py32 + verwenden, ist die Lösung ziemlich einfach:

test_suite="tests",
Saschpe
quelle
1
Ich wünschte, dies würde besser funktionieren, ich bin auf folgendes Problem gestoßen : stackoverflow.com/questions/6164004/… " Testnamen sollten mit Modulnamen übereinstimmen. Wenn es einen" foo_test.py "-Test gibt, muss es ein entsprechendes Modul foo.py geben . "
Charles L.
1
Genau. In meinem Fall, in dem ich ein externes Python teste, in dem es buchstäblich kein solches Python-Modul mit einer .py gibt, scheint es keinen guten Weg zu geben, dies zu erreichen.
Tom Swirly
2
Dies ist die richtige Lösung. Ich hatte das Problem @CharlesL nicht. hätten. Alle meine Tests sind benannt test_*.py. Außerdem habe ich herausgefunden, dass das angegebene Verzeichnis tatsächlich rekursiv durchsucht wird, um eine erweiterte Klasse zu finden unittest.TestCast. Dies ist äußerst nützlich, wenn Sie eine Verzeichnisstruktur haben, in der Sie tests/first_batch/test_*.pyund haben tests/second_batch/test_*.py. Sie können einfach angeben, test_suite="tests",und es wird alles rekursiv aufgenommen. Beachten Sie, dass jedes verschachtelte Verzeichnis eine __init__.pyDatei enthalten muss.
dcmm88
39

Vom Erstellen und Verteilen von Paketen mit Setuptools (Schwerpunkt Mine):

test_suite

Eine Zeichenfolge, die eine unittest.TestCase-Unterklasse (oder ein Paket oder Modul, das eine oder mehrere davon enthält, oder eine Methode einer solchen Unterklasse) benennt oder eine Funktion benennt , die ohne Argumente aufgerufen werden kann und eine unittest.TestSuite zurückgibt .

Daher setup.pywürden Sie in eine Funktion hinzufügen, die eine TestSuite zurückgibt:

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

Dann würden Sie den Befehl setupwie folgt angeben :

setup(
    ...
    test_suite='setup.my_test_suite',
    ...
)
Michael G.
quelle
3
Es gibt ein Problem mit dieser Lösung, weil sie 2 "Ebenen" von unittest erzeugt. Dies bedeutet, dass setuptools einen 'test'-Befehl erstellt, der versucht, eine TestSuite aus setup.my_test_suite zu erstellen, wodurch er gezwungen wird, setup.py zu importieren, wodurch das setup () erneut ausgeführt wird! Dieses zweite Mal wird ein neuer (verschachtelter) Testbefehl erstellt, der den gewünschten Test ausführt. Dies fällt den meisten Menschen möglicherweise nicht auf, aber wenn Sie versuchen, den Testbefehl zu erweitern (ich musste ihn ändern, weil ich meine Tests nicht "direkt" ausführen kann), können seltsame Probleme auftreten. Verwenden Sie stattdessen stackoverflow.com/a/21726329/3272850
dcmm88
2
Dies führt dazu, dass die Tests aus den oben genannten Gründen zweimal für mich ausgeführt werden. Es wurde behoben, indem die Funktion in __init__.pyden Testordner verschoben und darauf verwiesen wurde.
Anonym
3
Das Problem, dass Tests zweimal ausgeführt werden, kann leicht behoben werden, indem die setup()Funktion innerhalb des if __name__ == '__main__':Blocks im setup.pySkript ausgeführt wird. Wenn das Setup-Skript zum ersten Mal ausgeführt wird, wird der if-Block aufgerufen. Beim zweiten Mal wird das Setup-Skript als Modul importiert, sodass der if-Block nicht aufgerufen wird.
hoefling
Hmm, mir ist klar, dass meine setup.py diesen test_suiteParameter überhaupt NICHT enthält , aber "python setup.py test" funktioniert immer noch gut für mich. Das ist etwas anderes, was die Dokumentation sagt : „Wenn Sie nicht ein test_suite in Ihrem Setup () -Aufruf, festgelegt haben und bieten keine --test-Suite Option, tritt ein Fehler auf.“ Irgendeine Idee?
RayLuo
21

Sie benötigen keine Konfiguration, um dies zum Laufen zu bringen. Grundsätzlich gibt es zwei Möglichkeiten:

Der schnelle Weg

Benennen Sie Ihr test_module.pyin um module_test.py(im Grunde genommen _testals Suffix zu Tests für ein bestimmtes Modul hinzufügen ), und Python findet es automatisch. Stellen Sie einfach sicher, dass Sie Folgendes hinzufügen setup.py:

from setuptools import setup, find_packages

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

Der lange Weg

So geht's mit Ihrer aktuellen Verzeichnisstruktur:

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

Unter tests/__init__.pymöchten Sie das unittestund Ihr Unit-Test-Skript importieren test_moduleund anschließend eine Funktion zum Ausführen der Tests erstellen. In tests/__init__.py, in etwa wie folgt eingeben:

import unittest
import test_module

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

Die TestLoaderKlasse hat außerdem andere Funktionen loadTestsFromModule. Sie können laufen dir(unittest.TestLoader), um die anderen zu sehen, aber dieser ist am einfachsten zu verwenden.

Da Ihre Verzeichnisstruktur so ist, möchten Sie wahrscheinlich, dass Sie test_moduleIhr moduleSkript importieren können . Möglicherweise haben Sie dies bereits getan, aber falls Sie dies nicht getan haben, können Sie den übergeordneten Pfad angeben, damit Sie das packageModul und das moduleSkript importieren können . Geben Sie oben Folgendes ein test_module.py:

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

import unittest
import package.module
...

Fügen Sie dann schließlich setup.pydas testsModul hinzu und führen Sie den von Ihnen erstellten Befehl aus my_module_suite:

from setuptools import setup, find_packages

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

Dann rennst du einfach python setup.py test.

Hier ist ein Beispiel, das jemand als Referenz gemacht hat.

Antimaterie
quelle
2
Die Frage war, wie "python setup.py test" die Erkennungsfunktion von unittest nutzen kann. Damit wird das überhaupt nicht angesprochen.
Mikenerone
Ugh ... ja, ich dachte völlig, die Frage würde etwas anderes stellen. Ich bin nicht sicher, wie das passiert ist, ich muss meinen Verstand verlieren :(
Antimaterie
5

Eine mögliche Lösung besteht darin, den testBefehl für distutilsund setuptools/ einfach zu erweitern distribute. Dies scheint ein totaler Kluge und viel komplizierter zu sein, als ich es vorziehen würde, aber es scheint, dass alle Tests in meinem Paket beim Ausführen korrekt erkannt und ausgeführt werden python setup.py test. Ich halte mich zurück, um dies als Antwort auf meine Frage auszuwählen, in der Hoffnung, dass jemand eine elegantere Lösung bietet :)

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

Beispiel 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
quelle
3

Eine weitere weniger als ideale Lösung, die leicht von http://hg.python.org/unittest2/file/2b6411b9a838/unittest2/collector.py inspiriert wurde

Fügen Sie ein Modul hinzu, das einen TestSuiteder erkannten Tests zurückgibt . Konfigurieren Sie dann das Setup, um dieses Modul aufzurufen.

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

Hier ist 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)

Und hier ist 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
quelle
3

Das Standardbibliotheksmodul von Python unittestunterstützt die Erkennung (in Python 2.7 und höher sowie in Python 3.2 und höher). Wenn Sie diese Mindestversionen annehmen können, können Sie dem discoverBefehl einfach das Befehlszeilenargument hinzufügen unittest.

Es ist nur eine kleine Änderung erforderlich, um 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,
    },
)
Mikeneron
quelle
Übrigens gehe ich oben davon aus, dass Sie nur auf Python-Versionen abzielen, die tatsächlich die Erkennung unterstützen (2.7 und 3.2+), da sich die Frage speziell auf diese Funktion bezieht. Sie können die Beilage natürlich auch in eine Versionsprüfung einwickeln, wenn Sie auch mit älteren Versionen kompatibel bleiben möchten (und in diesen Fällen den Standardlader von setuptools verwenden).
Mikenerone
0

Dadurch wird run_tests.py nicht entfernt, es funktioniert jedoch mit setuptools. Hinzufügen:

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

Dann in setup.py: (Ich nehme an, Sie machen so etwas wie setup(**config))

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

Der einzige Nachteil, den ich sehe, ist, dass die Semantik von loadTestsFromNamesgebogen wird, aber der Befehl setuptools test ist der einzige Verbraucher und ruft ihn auf eine bestimmte Weise auf .

jwelsh
quelle