Jak uruchomić wszystkie testy jednostkowe Pythona w katalogu?

315

Mam katalog, który zawiera moje testy jednostkowe w języku Python. Każdy moduł testu jednostkowego ma formę testu _ *. Py . Próbuję utworzyć plik o nazwie all_test.py , który, jak się , uruchomi wszystkie pliki w wyżej wymienionym formularzu testowym i zwróci wynik. Do tej pory wypróbowałem dwie metody; oba zawiodły. Pokażę te dwie metody i mam nadzieję, że ktoś wie, jak to zrobić poprawnie.

Podczas mojej pierwszej dzielnej próby pomyślałem: „Jeśli po prostu zaimportuję wszystkie moje moduły testowe do pliku, a następnie unittest.main()wywołam ten szablon, to zadziała, prawda?” Okazuje się, że się myliłem.

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

To nie zadziałało, otrzymałem wynik:

$ python all_test.py 

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

OK

Przy drugiej próbie pomyślałem, ok, może spróbuję wykonać całą tę testowanie w bardziej „ręczny” sposób. Więc próbowałem to zrobić poniżej:

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

To również nie działało, ale wydaje się tak blisko!

$ 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

Wydaje mi się, że mam jakiś pakiet i mogę wykonać wynik. Jestem trochę zaniepokojony faktem, że mówi, że mam run=1, wydaje się, że tak powinno być run=2, ale jest to postęp. Ale jak przekazać i wyświetlić wynik jako główny? Lub w jaki sposób mogę go w zasadzie uruchomić, aby móc po prostu uruchomić ten plik, a tym samym uruchomić wszystkie testy jednostkowe w tym katalogu?

Stephen Cagle
źródło
1
Przejdź do odpowiedzi Travisa, jeśli używasz języka Python 2.7+
Rocky
czy kiedykolwiek próbowałeś uruchomić testy z obiektu instancji testowej?
Pinokio
Zobacz tę odpowiedź, aby uzyskać rozwiązanie z przykładową strukturą plików.
Derek Soike

Odpowiedzi:

477

W Pythonie 2.7 i nowszych wersjach nie musisz pisać nowego kodu ani korzystać z narzędzi innych firm; wykonywanie testu rekurencyjnego za pomocą wiersza poleceń jest wbudowane. Umieść __init__.pyw katalogu testowym i:

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

Możesz przeczytać więcej w niezbyt udanej dokumentacji Pythona 2.7 lub Pythona 3.x.

Travis Bear
źródło
11
problemy obejmują: ImportError: Nie można importować katalogu Start:
zinking
6
Przynajmniej w Pythonie 2.7.8 w Linuksie żadne wywołanie wiersza poleceń nie daje mi rekurencji. Mój projekt ma kilka podprojektów, których testy jednostkowe znajdują się w odpowiednich katalogach „unit_tests / <subproject> / python /”. Jeśli podam taką ścieżkę, uruchamiane są testy jednostkowe dla tego podprojektu, ale tylko z argumentem „unit_tests” jako argumentem katalogu testowego nie znaleziono żadnych testów (zamiast wszystkich testów dla wszystkich podprojektów, jak miałem nadzieję). Jakaś wskazówka?
user686249,
6
Informacje o rekursji: pierwsze polecenie bez <katalog_testowy> domyślnie ma wartość „.” i powraca do podmodułów . Oznacza to, że wszystkie katalogi testów, które chcesz odkryć, muszą mieć plik inicjujący .py. Jeśli to zrobią, zostaną odnalezione przez polecenie Discover. Po prostu spróbowałem, zadziałało.
Emil Stenström
To zadziałało dla mnie. Mam folder testów z czterema plikami, uruchom to z mojego terminalu Linux, świetne rzeczy.
JasTonAChair
5
Dzięki! Dlaczego nie jest to zaakceptowana odpowiedź? Moim zdaniem lepszą odpowiedzią jest zawsze ta, która nie wymaga żadnych zewnętrznych zależności ...
Jonathan Benn
108

Możesz użyć testera, który zrobiłby to za Ciebie. nos jest na przykład bardzo dobry. Po uruchomieniu znajdzie testy w bieżącym drzewie i je uruchomi.

Zaktualizowano:

Oto kod z moich dni przed nosem. Prawdopodobnie nie chcesz jawnej listy nazw modułów, ale być może reszta przyda ci się.

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)
Ned Batchelder
źródło
2
Czy zaletą tego podejścia jest po prostu bezpośrednie importowanie wszystkich modułów testowych do jednego modułu test_all.py i wywoływanie unittest.main (), że można opcjonalnie zadeklarować pakiet testowy w niektórych modułach, a nie w innych?
Corey Porter,
1
Wypróbowałem nos i działa idealnie. Łatwo go zainstalować i uruchomić w moim projekcie. Byłem nawet w stanie zautomatyzować go za pomocą kilku linii skryptu, uruchomionych w virtualenv. +1 za nos!
Jesse Webb
Nie zawsze jest to wykonalne: czasami importowanie struktury projektu może prowadzić do pomyłek, jeśli spróbuje uruchomić importowanie na modułach.
chiffa,
4
Zauważ, że nos był „w trybie konserwacji od kilku lat” i obecnie zaleca się stosowanie nose2 , pytest lub po prostu unittest / unittest2 w nowych projektach.
Kurt Peek
czy kiedykolwiek próbowałeś uruchomić testy z obiektu instancji testowej?
Pinokio
95

W Pythonie 3, jeśli używasz unittest.TestCase:

  • Musisz mieć pusty (lub inny) __init__.pyplik w swoim testkatalogu ( musi mieć nazwę test/)
  • Twoje pliki testowe wewnątrz test/pasują do wzorca test_*.py. Mogą znajdować się w podkatalogu pod test/, a te podkatalogi można nazwać jakkolwiek.

Następnie możesz uruchomić wszystkie testy za pomocą:

python -m unittest

Gotowy! Rozwiązanie mniej niż 100 linii. Mam nadzieję, że inny początkujący python oszczędza czas, znajdując to.

kod tmck
źródło
3
Zauważ, że domyślnie wyszukuje tylko testy w nazwach plików rozpoczynających się od „test”
Shawabawa,
3
Zgadza się, oryginalne pytanie odnosiło się do faktu, że „Każdy moduł testu jednostkowego ma formę testu _ *. Py.”, Więc odpowiedź ta jest bezpośrednią odpowiedzią. Zaktualizowałem odpowiedź, aby była bardziej jednoznaczna
kod tmck
1
Dzięki, że tego, czego mi brakowało, by skorzystać z odpowiedzi Travisa Beara.
Jeremy Cochoy,
65

Jest to teraz możliwe bezpośrednio z unittest: 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)
ubój98
źródło
3
Próbowałem również tej metody, mam kilka testów, ale działa idealnie. Świetny!!! Ale jestem ciekawy, że mam tylko 4 testy. Razem działają 0,032, ale kiedy używam tej metody do uruchomienia ich wszystkich, otrzymuję wynik .... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OKDlaczego? Różnica, skąd pochodzi?
simkus
Mam problem z uruchomieniem pliku, który wygląda tak z wiersza polecenia. Jak należy się na nią powołać?
Dustin Michels,
python file.py
rzeź
1
Działa bezbłędnie! Po prostu ustaw go w swoim teście / katalogu, a następnie ustaw start_id = "./". IMHO, ta odpowiedź jest teraz (Python 3.7) akceptowanym sposobem!
jjwdesign
Możesz zmienić ostatnią linię na ´res = runner.run (suite); sys.exit (0 jeśli res.wasSuccessful () else 1) ´ jeśli chcesz poprawnego kodu wyjścia
Sadap
32

Cóż, studiując nieco powyższy kod (konkretnie używając TextTestRunneri defaultTestLoader), byłem w stanie podejść bardzo blisko. Ostatecznie poprawiłem swój kod, przekazując wszystkie pakiety testowe do konstruktora pojedynczego zestawu, zamiast dodawać je „ręcznie”, co naprawiło moje inne problemy. Oto moje rozwiązanie.

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)

Tak, prawdopodobnie łatwiej jest po prostu użyć nosa niż to zrobić, ale jest to poza tym kwestia.

Stephen Cagle
źródło
dobrze, działa dobrze dla bieżącego katalogu, jak wywołać sub-bezpośrednio?
Larry Cai
Larry, zapoznaj się z nową odpowiedzią ( stackoverflow.com/a/24562019/104143 ) dotyczącą odkrywania testów rekurencyjnych
Peter Kofler
czy kiedykolwiek próbowałeś uruchomić testy z obiektu instancji testowej?
Pinokio
25

Jeśli chcesz uruchomić wszystkie testy z różnych klas przypadków testowych i z przyjemnością sprecyzujesz je, możesz to zrobić w następujący sposób:

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)

gdzie uclidjest mój projekt TestSymbolsi TestPatternssą podklasami TestCase.

szalony jeż
źródło
Z dokumentacji unittest.TestLoader : „Zwykle nie ma potrzeby tworzenia instancji tej klasy; moduł unittest zapewnia instancję, którą można udostępnić jako unittest.defaultTestLoader.” Ponieważ TestSuiteakceptuje iterowalność jako argument, możesz skonstruować tę iterowalność w pętli, aby uniknąć powtarzania loader.loadTestsFromTestCase.
Two-Bit Alchemist
@ Two-Bit Alchemist Twój drugi punkt jest szczególnie fajny. Zmieniłbym kod, aby zawierał, ale nie mogę go przetestować. (Pierwszy mod sprawiłby, że dla mojego upodobania wyglądałby zbyt podobnie do Javy ... chociaż zdaję sobie sprawę, że jestem irracjonalny (pieprzyć ich i nazwy zmiennych ich wielbłądów).
szalony jeż
To moja ulubiona, bardzo czysta. Był w stanie spakować to i uczynić z tego argument w mojej zwykłej linii poleceń.
MarkII
15

Użyłem tej discovermetody i przeciążenia, load_testsaby osiągnąć ten wynik w (minimalnie, jak sądzę) wierszach kodu:

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

Egzekucja na piątki

Ran 27 tests in 0.187s
OK
rds
źródło
jest to dostępne tylko dla python2.7, jak sądzę
Larry Cai
@larrycai Może zwykle używam Python 3, czasem Python 2.7. Pytanie nie było powiązane z konkretną wersją.
rds
Korzystam z języka Python 3.4 i Discover zwraca zestaw, dzięki czemu pętla staje się zbędna.
Wydmy
Dla przyszłych Larry'ego: „Wiele nowych funkcji zostało dodanych do unittest w Pythonie 2.7, w tym wykrywanie testów. Unittest2 pozwala korzystać z tych funkcji we wcześniejszych wersjach Pythona”.
Two-Bit Alchemist
8

Próbowałem różnych podejść, ale wszystkie wydają się wadliwe lub muszę zrobić jakiś kod, to denerwujące. Ale pod Linuksem istnieje dogodny sposób, aby po prostu znaleźć każdy test poprzez określony wzór, a następnie wywołać je jeden po drugim.

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

i co najważniejsze, zdecydowanie działa.

mrugać
źródło
7

W przypadku spakowanej biblioteki lub aplikacji nie chcesz tego robić. setuptools zrobi to za ciebie .

Aby użyć tego polecenia, testy projektu muszą być opakowane w unittestzestawie testów przez funkcję, klasę lub metodę TestCase albo moduł lub pakiet zawierający TestCaseklasy. Jeśli nazwany pakiet jest modułem, a moduł ma additional_tests()funkcję, jest wywoływany, a wynik (który musi być a unittest.TestSuite) jest dodawany do testów, które mają zostać uruchomione. Jeśli nazwany pakiet jest pakietem, wszelkie podmoduły i podpakiety są rekurencyjnie dodawane do całego zestawu testów .

Po prostu powiedz mu, gdzie jest twój główny pakiet testowy, na przykład:

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

I biegnij python setup.py test .

Wykrywanie na podstawie plików może być problematyczne w Pythonie 3, chyba że unikniesz importu względnego w pakiecie testowym, ponieważ discoverużywa importu plików. Mimo że obsługuje opcjonalne top_level_dir, ale miałem nieskończone błędy rekurencyjne. Tak więc prostym rozwiązaniem dla niepakowanego kodu jest umieszczenie następującego __init__.pypakietu testowego (patrz protokół load_tests ).

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
saaj
źródło
Ładna odpowiedź, i można go użyć do zautomatyzowania testu przed wdrożeniem! Dzięki
Arthur Clerc-Gherardi
4

Używam PyDev / LiClipse i tak naprawdę nie odkryłem, jak uruchomić wszystkie testy na raz z GUI. (edycja: kliknij prawym przyciskiem myszy główny folder testowy i wybierzRun as -> Python unit-test

Oto moje obecne obejście:

import unittest

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

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

Umieszczam ten kod w module o nazwie allw moim katalogu testowym. Jeśli uruchomię ten moduł jak Liittlipse, to wszystkie testy są uruchamiane. Jeśli poproszę o powtórzenie tylko określonych lub nieudanych testów, zostaną uruchomione tylko te testy. Nie przeszkadza mi to także w testowaniu linii poleceń (testy nosa) - jest ignorowany.

Konieczna może być zmiana argumentów na w discoverzależności od konfiguracji projektu.

Wydmy
źródło
Nazwy wszystkich plików testowych i metod testowych powinny zaczynać się od „test_”. W przeciwnym razie polecenie „Uruchom jako -> test jednostki Pythona” ich nie znajdzie.
Stefan
2

Na podstawie odpowiedzi Stephena Cagle'a dodałem obsługę zagnieżdżonych modułów testowych.

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)

Kod przeszukuje wszystkie podkatalogi w .poszukiwaniu *Tests.pyplików, które są następnie ładowane. Oczekuje, że każda *Tests.pyzawiera pojedynczą klasę, *Tests(unittest.TestCase)która jest ładowana kolejno i wykonywana jedna po drugiej.

Działa to z dowolnym głębokim zagnieżdżaniem katalogów / modułów, ale każdy katalog pomiędzy musi zawierać pusty __init__.py przynajmniej plik. Umożliwia to testowi załadowanie zagnieżdżonych modułów przez zastąpienie ukośników (lub odwrotnych ukośników) kropkami (patrz replace_slash_by_dot).

Peter Kofler
źródło
2

To stare pytanie, ale dla mnie teraz (w 2019 r.) Działało:

python -m unittest *_test.py

Wszystkie moje pliki testowe znajdują się w tym samym folderze co pliki źródłowe i kończą się na _test.

Plasty Grove
źródło
1

Ten skrypt BASH wykona najbardziej niepotrzebny katalog testowy Pythona z DOWOLNEGO systemu plików, bez względu na katalog roboczy, w którym się znajdujesz: katalog roboczy zawsze będzie tam, gdzie ten testkatalog się znajduje.

WSZYSTKIE TESTY, niezależne $ PWD

unittest moduł Pythona jest wrażliwy na bieżący katalog, chyba że powiesz mu gdzie (używając discover -s opcji).

Jest to przydatne, gdy pozostajesz w katalogu roboczym ./srclub ./exampleroboczym i potrzebujesz szybkiego ogólnego testu jednostkowego:

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

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

WYBRANE TESTY, niezależne $ PWD

Nazywam ten plik narzędziowy runone.pyi używam go w następujący sposób:

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)

Nie ma potrzeby, aby test/__init__.pyplik obciążał pakiet / obciążenie pamięci podczas produkcji.

John Greene
źródło
-3

Oto moje podejście, tworząc opakowanie do uruchamiania testów z wiersza poleceń:

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

Dla uproszczenia, proszę wybaczyć moje standardy kodowania inne niż PEP8 .

Następnie możesz utworzyć klasę BaseTest dla typowych komponentów dla wszystkich testów, aby każdy test wyglądał po prostu:

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

Aby uruchomić, wystarczy podać testy jako część argumentów wiersza poleceń, np .:

./run_tests.py -h http://example.com/ tests/**/*.py
kenorb
źródło
2
większość tej odpowiedzi nie ma nic wspólnego z wykrywaniem testu (tj. logowaniem itp.). Przepełnienie stosu służy do odpowiadania na pytania, a nie do pokazywania niepowiązanego kodu.
Corey Goldberg,