Wie führe ich alle Python-Komponententests in einem Verzeichnis aus?


314

Ich habe ein Verzeichnis, das meine Python-Unit-Tests enthält. Jedes Unit-Test-Modul hat die Form test _ *. Py . Ich versuche, eine Datei mit dem Namen all_test.py zu erstellen , die, wie Sie es erraten haben, alle Dateien in der oben genannten Testform ausführt und das Ergebnis zurückgibt . Ich habe bisher zwei Methoden ausprobiert; beide sind gescheitert. Ich werde die beiden Methoden zeigen, und ich hoffe, jemand da draußen weiß, wie man das tatsächlich richtig macht.

Bei meinem ersten tapferen Versuch dachte ich: "Wenn ich nur alle meine unittest.main()Testmodule in die Datei importiere und dann diesen Doodad aufrufe, funktioniert es, oder?" Es stellte sich heraus, dass ich falsch lag.

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

Das hat nicht funktioniert, das Ergebnis war:

$ python all_test.py 

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Für meinen zweiten Versuch werde ich allerdings versuchen, diese ganze Testsache "manueller" zu machen. Also habe ich versucht, das unten zu tun:

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

Das hat auch nicht funktioniert, aber es scheint so nah!

$ 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

Ich habe anscheinend eine Suite, und ich kann das Ergebnis ausführen. Ich bin ein wenig besorgt über die Tatsache, dass es heißt, ich habe nur run=1, scheint so zu sein run=2, aber es ist Fortschritt. Aber wie übergebe ich das Ergebnis und zeige es an main an? Oder wie kann ich es im Grunde zum Laufen bringen, damit ich diese Datei einfach ausführen und dabei alle Komponententests in diesem Verzeichnis ausführen kann?


1
Fahren Sie mit Travis 'Antwort fort, wenn Sie Python 2.7+
Rocky

Haben Sie jemals versucht, die Tests von einem Testinstanzobjekt aus auszuführen?
Pinocchio

In dieser Antwort finden Sie eine Lösung mit einer Beispieldateistruktur.
Derek Soike

Antworten:


477

Mit Python 2.7 und höher müssen Sie keinen neuen Code schreiben oder Tools von Drittanbietern verwenden, um dies zu tun. Die rekursive Testausführung über die Befehlszeile ist integriert. Legen Sie ein __init__.pyin Ihr Testverzeichnis und:

python -m unittest discover <test_directory>
# or
python -m unittest discover -s <directory> -p '*_test.py'

Weitere Informationen finden Sie in der unittest-Dokumentation zu Python 2.7 oder Python 3.x.


11
Zu den Problemen gehören: ImportError: Das Startverzeichnis kann nicht importiert werden:
Zinking

6
Zumindest mit Python 2.7.8 unter Linux gibt mir keiner der Befehlszeilenaufrufe eine Rekursion. Mein Projekt hat mehrere Teilprojekte, deren Komponententests in den jeweiligen Verzeichnissen "unit_tests / <subproject> / python /" gespeichert sind. Wenn ich einen solchen Pfad spezifiziere, werden die Komponententests für dieses Teilprojekt ausgeführt, aber mit nur "unit_tests" als Testverzeichnisargument werden keine Tests gefunden (anstelle aller Tests für alle Teilprojekte, wie ich gehofft habe). Irgendein Hinweis?
user686249

6
Informationen zur Rekursion: Der erste Befehl ohne <Testverzeichnis> ist standardmäßig "." und rekursiv zu Submodulen . Das heißt, alle Testverzeichnisse, die Sie entdecken möchten, müssen eine init .py haben. Wenn dies der Fall ist, werden sie vom Befehl remove gefunden. Ich habe es einfach versucht, es hat funktioniert.
Emil Stenström

Das hat bei mir funktioniert. Ich habe einen Testordner mit vier Dateien, führe diesen von meinem Linux-Terminal aus, tolles Zeug.
JasTonAChair

5
Vielen Dank! Warum ist das nicht die akzeptierte Antwort? Meiner Ansicht nach ist die bessere Antwort immer die, die keine externen Abhängigkeiten erfordert ...
Jonathan Benn

108

Sie könnten einen Testläufer verwenden, der dies für Sie erledigt. Nase ist zum Beispiel sehr gut. Beim Ausführen werden Tests im aktuellen Baum gefunden und ausgeführt.

Aktualisiert:

Hier ist ein Code aus meiner Zeit vor der Nase. Sie möchten wahrscheinlich nicht die explizite Liste der Modulnamen, aber vielleicht ist der Rest für Sie nützlich.

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
Ist der Vorteil dieses Ansatzes gegenüber dem expliziten Importieren aller Ihrer Testmodule in ein test_all.py-Modul und dem Aufruf von unittest.main (), dass Sie optional eine Testsuite in einigen Modulen und nicht in anderen deklarieren können?
Corey Porter

1
Ich habe die Nase ausprobiert und es funktioniert perfekt. Es war einfach in meinem Projekt zu installieren und auszuführen. Ich konnte es sogar mit ein paar Skriptzeilen automatisieren, die in einer virtuellen Umgebung ausgeführt wurden. +1 für die Nase!
Jesse Webb

Nicht immer machbar: Manchmal kann das Importieren der Projektstruktur dazu führen, dass die Nase verwirrt wird, wenn versucht wird, die Importe auf Modulen auszuführen.
Chiffa

4
Beachten Sie, dass sich die Nase "seit einigen Jahren im Wartungsmodus" befindet und derzeit empfohlen wird, für neue Projekte Nase2 , Pytest oder einfach nur unittest / unittest2 zu verwenden.
Kurt Peek

Haben Sie jemals versucht, die Tests von einem Testinstanzobjekt aus auszuführen?
Pinocchio

95

Wenn Sie in Python 3 Folgendes verwenden unittest.TestCase:

  • Sie müssen eine leere (oder anderweitige) __init__.pyDatei in Ihrem testVerzeichnis haben ( muss benannt werden test/)
  • Ihre darin enthaltenen Testdateien test/stimmen mit dem Muster überein test_*.py. Sie können sich in einem Unterverzeichnis unter befinden test/, und diese Unterverzeichnisse können wie alles benannt werden.

Anschließend können Sie alle Tests ausführen mit:

python -m unittest

Erledigt! Eine Lösung mit weniger als 100 Zeilen. Hoffentlich spart ein anderer Python-Anfänger Zeit, indem er dies findet.


3
Beachten Sie, dass standardmäßig nur nach Tests in Dateinamen
gesucht wird, die

3
Das ist richtig, die ursprüngliche Frage bezog sich auf die Tatsache, dass "jedes Unit-Test-Modul die Form test _ *. Py." Hat, also diese Antwort in direkter Antwort. Ich habe jetzt die Antwort aktualisiert, um expliziter zu sein
tmck-code

1
Danke, das, was mir fehlte, um Travis Bears Antwort zu verwenden.
Jeremy Cochoy

65

Dies ist jetzt direkt von unittest aus möglich: 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
Ich habe diese Methode auch ausprobiert, habe ein paar Tests, funktioniert aber perfekt. Ausgezeichnet!!! Aber ich bin neugierig, dass ich nur 4 Tests habe. Zusammen führen sie 0.032s aus, aber wenn ich diese Methode verwende, um sie alle auszuführen, erhalte ich das Ergebnis .... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OKWarum? Der Unterschied, woher kommt es?
Simkus

Ich habe Probleme beim Ausführen einer Datei, die so aussieht, über die Befehlszeile. Wie soll es aufgerufen werden?
Dustin Michels

python file.py
Schlachtung98

1
Hat einwandfrei funktioniert! Stellen Sie es einfach in Ihrem Test / Verzeichnis ein und setzen Sie dann die start_id = "./". IMHO, diese Antwort ist jetzt (Python 3.7) der akzeptierte Weg!
jjwdesign

Sie können die letzte Zeile in ´res = Runner.run (Suite) ändern. sys.exit (0 wenn res.wasSuccessful () else 1) ´ wenn Sie einen korrekten Exit-Code wollen
Sadap

32

Nun, indem ich den obigen Code ein wenig studierte (speziell mit TextTestRunnerund defaultTestLoader), konnte ich ziemlich nahe kommen. Schließlich habe ich meinen Code behoben, indem ich auch alle Testsuiten an einen einzelnen Suites-Konstruktor übergeben habe, anstatt sie "manuell" hinzuzufügen, wodurch meine anderen Probleme behoben wurden. Also hier ist meine Lösung.

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)

Ja, es ist wahrscheinlich einfacher, nur die Nase zu benutzen, als dies zu tun, aber das ist nicht der Punkt.


gut, es funktioniert gut für das aktuelle Verzeichnis, wie man das Sub-Direkt aufruft?
Larry Cai

Larry, siehe die neue Antwort ( stackoverflow.com/a/24562019/104143 ) für die rekursive Testentdeckung
Peter Kofler

Haben Sie jemals versucht, die Tests von einem Testinstanzobjekt aus auszuführen?
Pinocchio

25

Wenn Sie alle Tests aus verschiedenen Testfallklassen ausführen möchten und diese gerne explizit angeben, können Sie dies folgendermaßen tun:

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)

Wo uclidist mein Projekt und TestSymbolsund TestPatternssind Unterklassen von TestCase.


Aus den unittest.TestLoader-Dokumenten : "Normalerweise muss keine Instanz dieser Klasse erstellt werden. Das unittest-Modul stellt eine Instanz bereit, die als unittest.defaultTestLoader freigegeben werden kann." Da TestSuiteeine iterable als Argument akzeptiert wird , können Sie diese iterable in einer Schleife erstellen, um Wiederholungen zu vermeiden loader.loadTestsFromTestCase.
Zwei-Bit-Alchemist

@ Two-Bit Alchemist Ihr zweiter Punkt ist besonders schön. Ich würde den Code so ändern, dass er ihn enthält, aber ich kann ihn nicht testen. (Der erste Mod würde dafür sorgen, dass es für meinen Geschmack zu sehr nach Java aussieht. Obwohl mir klar wird, dass ich irrational bin (schrauben Sie sie und ihre Variablennamen für Kamelfälle)).
wahnsinniger Igel

Das ist mein Favorit, sehr sauber. Konnte dies packen und es zu einem Argument in meiner regulären Kommandozeile machen.
MarkII

15

Ich habe die discoverMethode und eine Überladung von verwendet load_tests, um dieses Ergebnis in einer (meiner Meinung nach minimalen) Anzahl von Codezeilen zu erzielen:

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

Hinrichtung auf fünf so etwas wie

Ran 27 tests in 0.187s
OK

Dies ist nur für Python2.7 verfügbar, denke ich
Larry Cai

@larrycai Vielleicht bin ich normalerweise auf Python 3, manchmal Python 2.7. Die Frage war nicht an eine bestimmte Version gebunden.
rds

Ich bin auf Python 3.4 und entdecke, dass eine Suite zurückgegeben wird, wodurch die Schleife überflüssig wird.
Dünen

Für zukünftige Larrys: "In Python 2.7 wurden viele neue Funktionen zu unittest hinzugefügt, einschließlich der Testerkennung . Mit unittest2 können Sie diese Funktionen mit früheren Versionen von Python verwenden."
Zwei-Bit-Alchemist

8

Ich habe verschiedene Ansätze ausprobiert, aber alle scheinen fehlerhaft zu sein, oder ich muss Code erstellen, das ist ärgerlich. Aber unter Linux gibt es einen bequemen Weg, einfach jeden Test anhand eines bestimmten Musters zu finden und ihn dann einzeln aufzurufen.

find . -name 'Test*py' -exec python '{}' \;

und vor allem funktioniert es definitiv.


7

Im Falle einer gepackten Bibliothek oder Anwendung möchten Sie dies nicht tun. setuptools werde es für dich tun .

Um diesen Befehl verwenden zu können, müssen die Tests Ihres Projekts unittestentweder von einer Funktion, einer TestCase-Klasse oder -Methode oder einem Modul oder Paket mit TestCaseKlassen in eine Testsuite eingeschlossen werden. Wenn die benannte Suite ein Modul ist und das Modul eine additional_tests()Funktion hat, wird sie aufgerufen und das Ergebnis (das a sein muss unittest.TestSuite) zu den auszuführenden Tests hinzugefügt. Wenn die genannte Suite ein Paket ist, werden alle Submodule und Unterpakete rekursiv zur gesamten Testsuite hinzugefügt .

Sagen Sie einfach, wo sich Ihr Root-Testpaket befindet, wie:

setup(
    # ...
    test_suite = 'somepkg.test'
)

Und renn python setup.py test.

Die discoverdateibasierte Erkennung kann in Python 3 problematisch sein, es sei denn, Sie vermeiden relative Importe in Ihre Testsuite, da der Dateiimport verwendet wird. Obwohl es optional unterstützt top_level_dir, hatte ich einige unendliche Rekursionsfehler. Eine einfache Lösung für einen nicht gepackten Code besteht darin, Folgendes in __init__.pyIhr Testpaket einzufügen (siehe load_tests-Protokoll ).

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

Gute Antwort, und es kann verwendet werden, um den Test vor der Bereitstellung zu automatisieren! Vielen Dank
Arthur Clerc-Gherardi

4

Ich verwende PyDev / LiClipse und habe nicht wirklich herausgefunden, wie alle Tests gleichzeitig über die GUI ausgeführt werden können. (Bearbeiten: Sie klicken mit der rechten Maustaste auf den Stammtestordner und wählenRun as -> Python unit-test

Dies ist meine aktuelle Problemumgehung:

import unittest

def load_tests(loader, tests, pattern):
    return loader.discover('.')

if __name__ == '__main__':
    unittest.main()

Ich habe diesen Code in ein Modul eingefügt, das allin meinem Testverzeichnis aufgerufen wird . Wenn ich dieses Modul als Unittest von LiClipse aus starte, werden alle Tests ausgeführt. Wenn ich nur bestimmte oder fehlgeschlagene Tests wiederholen möchte, werden nur diese Tests ausgeführt. Es stört auch meinen Befehlszeilentestläufer nicht (Nosetests) - es wird ignoriert.

Möglicherweise müssen Sie die Argumente discoverbasierend auf Ihrem Projekt-Setup ändern .


Die Namen aller Testdateien und Testmethoden sollten mit "test_" beginnen. Andernfalls werden sie vom Befehl "Ausführen als -> Python-Komponententest" nicht gefunden.
Stefan

2

Basierend auf der Antwort von Stephen Cagle habe ich Unterstützung für verschachtelte Testmodule hinzugefügt.

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)

Der Code durchsucht alle Unterverzeichnisse von .nach *Tests.pyDateien, die dann geladen werden. Es wird erwartet, dass jede *Tests.pyKlasse eine einzelne Klasse enthält, *Tests(unittest.TestCase)die nacheinander geladen und nacheinander ausgeführt wird.

Dies funktioniert mit einer willkürlichen tiefen Verschachtelung von Verzeichnissen / Modulen, aber jedes Verzeichnis dazwischen muss __init__.pymindestens eine leere Datei enthalten . Auf diese Weise kann der Test die verschachtelten Module laden, indem Schrägstriche (oder Backslashes) durch Punkte ersetzt werden (siehe replace_slash_by_dot).


2

Dies ist eine alte Frage, aber was jetzt (2019) für mich funktioniert hat, ist:

python -m unittest *_test.py

Alle meine Testdateien befinden sich im selben Ordner wie die Quelldateien und enden mit _test.



1

Dieses BASH-Skript führt das Python-Unittest-Testverzeichnis von ANYWHERE im Dateisystem aus, unabhängig davon, in welchem ​​Arbeitsverzeichnis Sie sich befinden: Das Arbeitsverzeichnis testbefindet sich immer dort, wo sich dieses Verzeichnis befindet.

ALLE TESTS, unabhängig $ PWD

Das unittest Python-Modul reagiert empfindlich auf Ihr aktuelles Verzeichnis, es sei denn, Sie geben an, wo (mit der discover -sOption).

Dies ist nützlich, wenn Sie im ./srcoder ./exampleArbeitsverzeichnis bleiben und einen schnellen Gesamttest benötigen:

#!/bin/bash
this_program="$0"
dirname="`dirname $this_program`"
readlink="`readlink -e $dirname`"

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

AUSGEWÄHLTE TESTS, unabhängige $ PWD

Ich benenne diese Dienstprogrammdatei: runone.pyund benutze sie wie folgt:

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)

Es ist keine test/__init__.pyDatei erforderlich, um den Paket- / Speicheraufwand während der Produktion zu belasten.


-3

Hier ist mein Ansatz, indem ich einen Wrapper erstelle , um Tests über die Befehlszeile auszuführen:

#!/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())

Entschuldigen Sie der Einfachheit halber meine Nicht- PEP8- Codierungsstandards.

Dann können Sie für alle Ihre Tests eine BaseTest-Klasse für allgemeine Komponenten erstellen, sodass jeder Ihrer Tests einfach so aussehen würde:

from BaseTest import BaseTest
class FooTestPagesBasic(BaseTest):
    def test_foo(self):
        driver = self.driver
        driver.get(self.base_url + "/")

Zum Ausführen geben Sie einfach Tests als Teil der Befehlszeilenargumente an, z.

./run_tests.py -h http://example.com/ tests/**/*.py

2
Der größte Teil dieser Antwort hat nichts mit der Testerkennung zu tun (z. B. Protokollierung usw.). Der Stapelüberlauf dient zur Beantwortung von Fragen und zeigt keinen unabhängigen Code an.
Corey Goldberg
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.