web-dev-qa-db-fra.com

Découverte récursive unittest

J'ai un package avec un répertoire "tests" dans lequel je stocke mes tests unitaires. Mon colis ressemble à:

.
├── LICENSE
├── models
│   └── __init__.py
├── README.md
├── requirements.txt
├── tc.py
├── tests
│   ├── db
│   │   └── test_employee.py
│   └── test_tc.py
└── todo.txt

À partir de mon répertoire de packages, je souhaite pouvoir trouver les deux tests/test_tc.py et tests/db/test_employee.py. Je préférerais ne pas avoir à installer une bibliothèque tierce (nose ou etc) ou avoir à construire manuellement un TestSuite pour l'exécuter.

Il y a sûrement un moyen de dire unittest discover ne pas arrêter de chercher une fois qu'il a trouvé un test? python -m unittest discover -s tests trouvera tests/test_tc.py et python -m unittest discover -s tests/db trouvera tests/db/test_employee.py. N'y a-t-il pas un moyen de trouver les deux?

33
Adam Smith

En creusant un peu, il semble que tant que les modules plus profonds restent importables, ils seront découverts via python -m unittest discover. La solution était alors simplement d'ajouter un __init__.py fichier dans chaque répertoire pour en faire des packages.

.
├── LICENSE
├── models
│   └── __init__.py
├── README.md
├── requirements.txt
├── tc.py
├── tests
│   ├── db
│   │   ├── __init__.py       # NEW
│   │   └── test_employee.py
│   ├── __init__.py           # NEW
│   └── test_tc.py
└── todo.txt

Tant que chaque répertoire a un __init__.py, python -m unittest discover peut importer les test_* module.

59
Adam Smith

Si vous êtes d'accord avec l'ajout d'un fichier __init__.py Dans les tests, vous pouvez y mettre une fonction load_tests Qui gérera la découverte pour vous.

Si un nom de package de test (répertoire avec __init__.py) Correspond au modèle, le package sera vérifié pour une fonction 'load_tests'. Si cela existe, il sera appelé avec loader, tests, pattern.

Si load_tests existe alors la découverte ne pas récursive dans le package, load_tests est responsable du chargement de tous les tests dans le package.

Je suis loin d'être convaincu que c'est la meilleure façon, mais une façon d'écrire cette fonction serait:

import os
import pkgutil
import inspect
import unittest

# Add *all* subdirectories to this module's path
__path__ = [x[0] for x in os.walk(os.path.dirname(__file__))]

def load_tests(loader, suite, pattern):
    for imp, modname, _ in pkgutil.walk_packages(__path__):
        mod = imp.find_module(modname).load_module(modname)
        for memname, memobj in inspect.getmembers(mod):
            if inspect.isclass(memobj):
                if issubclass(memobj, unittest.TestCase):
                    print("Found TestCase: {}".format(memobj))
                    for test in loader.loadTestsFromTestCase(memobj):
                        print("  Found Test: {}".format(test))
                        suite.addTest(test)

    print("=" * 70)
    return suite

Assez moche, je suis d'accord.

Tout d'abord, vous ajoutez tous les sous-répertoires au chemin du package de test ( Docs ).

Ensuite, vous utilisez pkgutil pour parcourir le chemin, à la recherche de packages ou de modules.

Quand il en trouve un, il vérifie ensuite les membres du module pour voir si ce sont des classes, et si ce sont des classes, si ce sont des sous-classes de unittest.TestCase. S'ils le sont, les tests à l'intérieur des classes sont chargés dans la suite de tests.

Alors maintenant, depuis l'intérieur de la racine de votre projet, vous pouvez taper

python -m unittest discover -p tests

Utilisation du commutateur de modèle -p. Si tout se passe bien, vous verrez ce que j'ai vu, qui ressemble à ceci:

Found TestCase: <class 'test_tc.TestCase'>
  Found Test: testBar (test_tc.TestCase)
  Found Test: testFoo (test_tc.TestCase)
Found TestCase: <class 'test_employee.TestCase'>
  Found Test: testBar (test_employee.TestCase)
  Found Test: testFoo (test_employee.TestCase)
======================================================================
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

C'est ce qui était attendu, chacun de mes deux fichiers d'exemple contenait deux tests, testFoo et testBar chacun.

Edit: Après quelques recherches supplémentaires, il semble que vous puissiez spécifier cette fonction comme:

def load_tests(loader, suite, pattern):
    for imp, modname, _ in pkgutil.walk_packages(__path__):
        mod = imp.find_module(modname).load_module(modname)
        for test in loader.loadTestsFromModule(mod):
            print("Found Tests: {}".format(test._tests))
            suite.addTests(test)

Cela utilise la méthode loader.loadTestsFromModule() au lieu de la méthode loader.loadTestsFromTestCase() que j'ai utilisée ci-dessus. Il modifie toujours le chemin du package tests et le parcourt à la recherche de modules, ce qui, je pense, est la clé ici.

La sortie semble un peu différente maintenant, car nous ajoutons une suite de tests trouvée à la fois à notre principale suite de tests suite:

python -m unittest discover -p tests
Found Tests: [<test_tc.TestCase testMethod=testBar>, <test_tc.TestCase testMethod=testFoo>]
Found Tests: [<test_employee.TestCase testMethod=testBar>, <test_employee.TestCase testMethod=testFoo>]
======================================================================
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Mais nous obtenons toujours les 4 tests que nous attendions, dans les deux classes, dans les deux sous-répertoires.

6
jedwards