Comment exécuter tous les tests unitaires Python dans un répertoire?

315

J'ai un répertoire qui contient mes tests unitaires Python. Chaque module de test unitaire est de la forme test _ *. Py . J'essaie de créer un fichier appelé all_test.py qui, vous l'aurez deviné, exécutera tous les fichiers du formulaire de test susmentionné et renverra le résultat. J'ai essayé jusqu'à présent deux méthodes; les deux ont échoué. Je vais montrer les deux méthodes, et j'espère que quelqu'un sait comment faire cela correctement.

Pour ma première vaillante tentative, je me suis dit: "Si je viens d'importer tous mes modules de test dans le fichier, puis d'appeler ce unittest.main()doodad, cela fonctionnera, non?" Eh bien, il s'est avéré que j'avais tort.

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

Cela n'a pas fonctionné, le résultat que j'ai obtenu a été:

$ python all_test.py 

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

OK

Pour mon deuxième essai, je pense, ok, peut-être que j'essaierai de faire tout ce test d'une manière plus "manuelle". J'ai donc essayé de le faire ci-dessous:

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

Cela n'a pas fonctionné non plus, mais cela semble si proche!

$ 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

Il me semble que j'ai une suite quelconque et je peux exécuter le résultat. Je suis un peu préoccupé par le fait qu'il dit que je n'ai que run=1, semble que cela devrait être run=2, mais c'est un progrès. Mais comment passer et afficher le résultat sur main? Ou comment puis-je le faire fonctionner pour que je puisse simplement exécuter ce fichier et, ce faisant, exécuter tous les tests unitaires dans ce répertoire?

Stephen Cagle
la source
1
Passez à la réponse de Travis si vous utilisez Python 2.7+
Rocky
avez-vous déjà essayé d'exécuter les tests à partir d'un objet d'instance de test?
Pinocchio
Voir cette réponse pour une solution avec un exemple de structure de fichier.
Derek Soike

Réponses:

477

Avec Python 2.7 et supérieur, vous n'avez pas besoin d'écrire de nouveau code ou d'utiliser des outils tiers pour ce faire; l'exécution de tests récursifs via la ligne de commande est intégrée. Mettez un __init__.pydans votre répertoire de test et:

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

Vous pouvez en savoir plus dans la documentation python 2.7 ou python 3.x unittest.

Travis Bear
la source
11
les problèmes incluent: ImportError: le répertoire de démarrage n'est pas importable:
zinking
6
Au moins avec Python 2.7.8 sous Linux, ni l'invocation de ligne de commande ne me donne de récursivité. Mon projet comporte plusieurs sous-projets dont les tests unitaires se trouvent dans les répertoires respectifs "unit_tests / <subproject> / python /". Si je spécifie un tel chemin, les tests unitaires pour ce sous-projet sont exécutés, mais avec juste "unit_tests" comme argument de répertoire de test, aucun test n'est trouvé (au lieu de tous les tests pour tous les sous-projets, comme je l'espérais). Un indice?
user686249
6
À propos de la récursivité: la première commande sans <répertoire_test> est par défaut "." et revient aux sous-modules . Autrement dit, tous les répertoires de tests que vous souhaitez découvrir doivent avoir un init .py. S'ils le font, ils seront trouvés par la commande de découverte. Je viens de l'essayer, cela a fonctionné.
Emil Stenström
Cela a fonctionné pour moi. J'ai un dossier de tests avec quatre fichiers, exécutez-le à partir de mon terminal Linux, très bien.
JasTonAChair
5
Merci! Pourquoi n'est-ce pas la réponse acceptée? À mon avis, la meilleure réponse est toujours celle qui ne nécessite aucune dépendance externe ...
Jonathan Benn
108

Vous pourriez utiliser un lanceur de test qui ferait cela pour vous. nez est très bon par exemple. Une fois exécuté, il trouvera des tests dans l'arborescence actuelle et les exécutera.

Actualisé:

Voici un code de mes jours avant le nez. Vous ne voulez probablement pas la liste explicite des noms de modules, mais peut-être que le reste vous sera utile.

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
la source
2
L'avantage de cette approche est-il simplement d'importer explicitement tous vos modules de test dans un module test_all.py et d'appeler unittest.main () que vous pouvez éventuellement déclarer une suite de tests dans certains modules et pas dans d'autres?
Corey Porter
1
J'ai essayé le nez et ça marche parfaitement. Il était facile à installer et à exécuter dans mon projet. J'ai même pu l'automatiser avec quelques lignes de script, en cours d'exécution dans un virtualenv. +1 pour le nez!
Jesse Webb
Pas toujours faisable: parfois, la structure d'importation du projet peut entraîner une confusion dans le nez s'il essaie d'exécuter les importations sur les modules.
chiffa
4
Notez que le nez est "en mode maintenance depuis plusieurs années" et il est actuellement conseillé d'utiliser nose2 , pytest , ou tout simplement unittest / unittest2 pour de nouveaux projets.
Kurt Peek
avez-vous déjà essayé d'exécuter les tests à partir d'un objet d'instance de test?
Pinocchio
96

En python 3, si vous utilisez unittest.TestCase:

  • Vous devez avoir un __init__.pyfichier vide (ou autre) dans votre testrépertoire ( doit être nommé test/)
  • Vos fichiers de test à l'intérieur test/correspondent au modèle test_*.py. Ils peuvent se trouver dans un sous-répertoire sous test/et ces sous-répertoires peuvent être nommés comme n'importe quoi.

Ensuite, vous pouvez exécuter tous les tests avec:

python -m unittest

Terminé! Une solution à moins de 100 lignes. Espérons qu'un autre débutant en python gagne du temps en trouvant cela.

code tmck
la source
3
Notez que par défaut, il recherche uniquement les tests dans les noms de fichiers commençant par "test"
Shawabawa
3
C'est exact, la question d'origine faisait référence au fait que "chaque module de test unitaire est de la forme test _ *. Py.", Donc cette réponse en réponse directe. J'ai maintenant mis à jour la réponse pour être plus explicite
tmck-code
1
Merci, c'est ce qui me manquait pour utiliser la réponse de Travis Bear.
Jeremy Cochoy
65

Ceci est désormais possible directement depuis 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)
slaughter98
la source
3
J'ai également essayé cette méthode, j'ai fait quelques tests, mais cela fonctionne parfaitement. Excellent!!! Mais je suis curieux de n'avoir que 4 tests. Ensemble, ils exécutent 0,032 s, mais lorsque j'utilise cette méthode pour les exécuter tous, j'obtiens un résultat .... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OKPourquoi? La différence, d'où ça vient?
simkus
J'ai du mal à exécuter un fichier qui ressemble à ceci à partir de la ligne de commande. Comment doit-il être invoqué?
Dustin Michels,
python file.py
slaughter98
1
Fonctionne parfaitement! Réglez-le simplement dans votre test / dir, puis définissez start_id = "./". À mon humble avis, cette réponse est maintenant (Python 3.7) la voie acceptée!
jjwdesign
Vous pouvez changer la dernière ligne en ´res = runner.run (suite); sys.exit (0 si res.wasSuccessful () else 1) ´ si vous voulez un code de sortie correct
Sadap
32

Eh bien, en étudiant un peu le code ci-dessus (en utilisant spécifiquement TextTestRunneret defaultTestLoader), j'ai pu être assez proche. Finalement, j'ai corrigé mon code en passant simplement toutes les suites de tests à un seul constructeur de suites, plutôt que de les ajouter "manuellement", ce qui a résolu mes autres problèmes. Voici donc ma solution.

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)

Oui, il est probablement plus facile d'utiliser simplement le nez que de le faire, mais c'est d'ailleurs le point.

Stephen Cagle
la source
bon, ça marche bien pour le répertoire courant, comment invoquer le sous-direct?
Larry Cai
Larry, voir la nouvelle réponse ( stackoverflow.com/a/24562019/104143 ) pour la découverte de tests récursifs
Peter Kofler
avez-vous déjà essayé d'exécuter les tests à partir d'un objet d'instance de test?
Pinocchio
25

Si vous souhaitez exécuter tous les tests à partir de différentes classes de cas de test et que vous êtes heureux de les spécifier explicitement, vous pouvez le faire comme ceci:

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)

uclidest mon projet et TestSymbolset TestPatternssont des sous-classes de TestCase.

hérisson dément
la source
À partir des documents unittest.TestLoader : "Normalement, il n'est pas nécessaire de créer une instance de cette classe; le module unittest fournit une instance qui peut être partagée en tant que unittest.defaultTestLoader." De plus, comme TestSuiteaccepte un itérable comme argument, vous pouvez construire cet itérable en boucle pour éviter de le répéter loader.loadTestsFromTestCase.
Two-Bit Alchemist
@ Two-Bit Alchemist votre deuxième point en particulier est agréable. Je changerais le code pour l'inclure mais je ne peux pas le tester. (Le premier mod le ferait ressembler trop à Java à mon goût .. même si je me rends compte que je suis irrationnel (vissez-les dans leurs noms de variables de cas de chameau)).
hérisson dément
Ceci est mon préféré, très propre. A pu empaqueter ceci et en faire un argument dans ma ligne de commande régulière.
MarkII
15

J'ai utilisé la discoverméthode et une surcharge de load_testspour obtenir ce résultat en un nombre de lignes de code (minimal, je pense):

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

Exécution sur cinq quelque chose comme

Ran 27 tests in 0.187s
OK
rds
la source
ceci n'est disponible que pour python2.7, je suppose
Larry Cai
@larrycai Peut-être, je suis généralement sur Python 3, parfois Python 2.7. La question n'était pas liée à une version spécifique.
rds
Je suis sur Python 3.4 et découvrez retourne une suite, rendant la boucle redondante.
Dunes
Pour les futurs Larry: "De nombreuses nouvelles fonctionnalités ont été ajoutées à unittest dans Python 2.7, y compris la découverte de tests. Unittest2 vous permet d'utiliser ces fonctionnalités avec des versions antérieures de Python."
Two-Bit Alchemist
8

J'ai essayé différentes approches mais toutes semblent défectueuses ou je dois créer du code, c'est ennuyeux. Mais il existe un moyen pratique sous Linux, qui consiste simplement à trouver chaque test à travers un certain modèle, puis à les invoquer un par un.

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

et surtout, cela fonctionne définitivement.

zink
la source
7

Dans le cas d'une bibliothèque ou d'une application packagée , vous ne voulez pas le faire. setuptools le fera pour vous .

Pour utiliser cette commande, les tests de votre projet doivent être encapsulés dans une unittestsuite de tests par une fonction, une classe ou une méthode TestCase, ou un module ou un package contenant des TestCaseclasses. Si la suite nommée est un module et que le module a une additional_tests()fonction, elle est appelée et le résultat (qui doit être a unittest.TestSuite) est ajouté aux tests à exécuter. Si la suite nommée est un package, tous les sous-modules et sous-packages sont récursivement ajoutés à la suite de tests globale .

Dites-lui simplement où se trouve votre package de test racine, comme:

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

Et courez python setup.py test.

La découverte basée sur des fichiers peut être problématique dans Python 3, sauf si vous évitez les importations relatives dans votre suite de tests, car discoverutilise l'importation de fichiers. Même s'il prend en charge en option top_level_dir, mais j'ai eu des erreurs de récursivité infinies. Ainsi, une solution simple pour un code non packagé est de mettre les éléments suivants dans __init__.pyvotre package de test (voir le protocole 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
la source
Belle réponse, et il peut être utilisé pour automatiser le test avant le déploiement! Merci
Arthur Clerc-Gherardi
4

J'utilise PyDev / LiClipse et je n'ai pas vraiment compris comment exécuter tous les tests à la fois à partir de l'interface graphique. (modifier: vous faites un clic droit sur le dossier de test racine et choisissezRun as -> Python unit-test

Voici ma solution de contournement actuelle:

import unittest

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

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

J'ai mis ce code dans un module appelé all dans mon répertoire de test. Si j'exécute ce module en tant qu'unité à partir de LiClipse, tous les tests sont exécutés. Si je demande de répéter uniquement des tests spécifiques ou ayant échoué, seuls ces tests sont exécutés. Cela n'interfère pas non plus avec mon lanceur de test en ligne de commande (nosetests) - il est ignoré.

Vous devrez peut-être modifier les arguments en discoverfonction de la configuration de votre projet.

Dunes
la source
Les noms de tous les fichiers et méthodes de test doivent commencer par "test_". Sinon, la commande "Exécuter en tant que -> Test unitaire Python" ne les trouvera pas.
Stefan
2

Sur la base de la réponse de Stephen Cagle, j'ai ajouté la prise en charge des modules de test imbriqués.

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)

Le code recherche tous les sous-répertoires de .pour les *Tests.pyfichiers qui sont ensuite chargés. Il s'attend *Tests.pyà ce que chacun contienne une seule classe *Tests(unittest.TestCase)qui est chargée à son tour et exécutée l'une après l'autre.

Cela fonctionne avec l'imbrication profonde arbitraire des répertoires / modules, mais chaque répertoire intermédiaire doit contenir __init__.pyau moins un fichier vide . Cela permet au test de charger les modules imbriqués en remplaçant les barres obliques (ou les contre-obliques) par des points (voir replace_slash_by_dot).

Peter Kofler
la source
2

C'est une vieille question, mais ce qui a fonctionné pour moi maintenant (en 2019) est:

python -m unittest *_test.py

Tous mes fichiers de test sont dans le même dossier que les fichiers source et se terminent par _test.

Plasty Grove
la source
1

Ce script BASH exécutera le répertoire de test python unittest de N'IMPORTE OH dans le système de fichiers, quel que soit le répertoire de travail dans lequel vous vous trouvez: son répertoire de travail sera toujours là où test répertoire.

TOUS LES TESTS, indépendant $ PWD

le module Python unittest est sensible à votre répertoire actuel, sauf si vous lui dites où (en utilisant discover -s option).

Ceci est utile lorsque vous restez dans le répertoire de travail ./srcou ./exampleet vous avez besoin d'un test unitaire global rapide:

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

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

ESSAIS SÉLECTIONNÉS, indépendants PWD $

Je nomme ce fichier utilitaire: runone.pyet je l'utilise comme ceci:

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)

Pas besoin de test/__init__.pyfichier pour surcharger votre package / surcharge de mémoire pendant la production.

John Greene
la source
-3

Voici mon approche en créant un wrapper pour exécuter des tests à partir de la ligne de commande:

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

Par souci de simplicité, veuillez excuser mes normes de codage non PEP8 .

Ensuite, vous pouvez créer la classe BaseTest pour les composants communs à tous vos tests, de sorte que chacun de vos tests ressemblerait simplement à:

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

Pour exécuter, vous spécifiez simplement des tests dans le cadre des arguments de la ligne de commande, par exemple:

./run_tests.py -h http://example.com/ tests/**/*.py
Kenorb
la source
2
la plupart de cette réponse n'a rien à voir avec la découverte de tests (c'est-à-dire la journalisation, etc.). Stack Overflow sert à répondre aux questions, sans afficher de code sans rapport.
Corey Goldberg